Compare commits

...

4 Commits

10 changed files with 585 additions and 417 deletions

258
ansible/ansible.go Normal file
View File

@ -0,0 +1,258 @@
package ansible
import (
"errors"
"fmt"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/thegeeklab/wp-plugin-go/v2/types"
"github.com/urfave/cli/v2"
"golang.org/x/sys/execabs"
)
const (
AnsibleForksDefault = 5
ansibleBin = "/usr/local/bin/ansible"
ansibleGalaxyBin = "/usr/local/bin/ansible-galaxy"
ansiblePlaybookBin = "/usr/local/bin/ansible-playbook"
)
var ErrAnsiblePlaybookNotFound = errors.New("no playbook found")
type Ansible struct {
GalaxyRequirements string
Inventories cli.StringSlice
Playbooks cli.StringSlice
Limit string
SkipTags string
StartAtTask string
Tags string
ExtraVars cli.StringSlice
ModulePath cli.StringSlice
Check bool
Diff bool
FlushCache bool
ForceHandlers bool
ListHosts bool
ListTags bool
ListTasks bool
SyntaxCheck bool
Forks int
VaultID string
VaultPasswordFile string
Verbose int
PrivateKeyFile string
User string
Connection string
Timeout int
SSHCommonArgs string
SFTPExtraArgs string
SCPExtraArgs string
SSHExtraArgs string
Become bool
BecomeMethod string
BecomeUser string
}
// Version runs the Ansible binary with the --version flag to retrieve the current version.
func (a *Ansible) Version() *types.Cmd {
args := []string{
"--version",
}
return &types.Cmd{
Cmd: execabs.Command(ansibleBin, args...),
}
}
// GetPlaybooks retrieves the list of Ansible playbook files based on the configured playbook patterns.
func (a *Ansible) GetPlaybooks() error {
var playbooks []string
for _, pb := range a.Playbooks.Value() {
files, err := filepath.Glob(pb)
if err != nil {
playbooks = append(playbooks, pb)
continue
}
playbooks = append(playbooks, files...)
}
if len(playbooks) == 0 {
log.Debug().Strs("patterns", a.Playbooks.Value()).Msg("no playbooks found")
return ErrAnsiblePlaybookNotFound
}
a.Playbooks = *cli.NewStringSlice(playbooks...)
return nil
}
// GalaxyInstall runs the ansible-galaxy install command with the configured options.
func (a *Ansible) GalaxyInstall() *types.Cmd {
args := []string{
"install",
"--force",
"--role-file",
a.GalaxyRequirements,
}
if a.Verbose > 0 {
args = append(args, fmt.Sprintf("-%s", strings.Repeat("v", a.Verbose)))
}
return &types.Cmd{
Cmd: execabs.Command(ansibleGalaxyBin, args...),
}
}
// Play runs the Ansible playbook with the configured options.
//
//nolint:gocyclo
func (a *Ansible) Play() *types.Cmd {
args := make([]string, 0)
for _, inventory := range a.Inventories.Value() {
args = append(args, "--inventory", inventory)
}
if len(a.ModulePath.Value()) > 0 {
args = append(args, "--module-path", strings.Join(a.ModulePath.Value(), ":"))
}
if a.VaultID != "" {
args = append(args, "--vault-id", a.VaultID)
}
if a.VaultPasswordFile != "" {
args = append(args, "--vault-password-file", a.VaultPasswordFile)
}
for _, v := range a.ExtraVars.Value() {
args = append(args, "--extra-vars", v)
}
if a.ListHosts {
args = append(args, "--list-hosts")
args = append(args, a.Playbooks.Value()...)
return &types.Cmd{
Cmd: execabs.Command(ansiblePlaybookBin, args...),
}
}
if a.SyntaxCheck {
args = append(args, "--syntax-check")
args = append(args, a.Playbooks.Value()...)
return &types.Cmd{
Cmd: execabs.Command(ansiblePlaybookBin, args...),
}
}
if a.Check {
args = append(args, "--check")
}
if a.Diff {
args = append(args, "--diff")
}
if a.FlushCache {
args = append(args, "--flush-cache")
}
if a.ForceHandlers {
args = append(args, "--force-handlers")
}
if a.Forks != AnsibleForksDefault {
args = append(args, "--forks", strconv.Itoa(a.Forks))
}
if a.Limit != "" {
args = append(args, "--limit", a.Limit)
}
if a.ListTags {
args = append(args, "--list-tags")
}
if a.ListTasks {
args = append(args, "--list-tasks")
}
if a.SkipTags != "" {
args = append(args, "--skip-tags", a.SkipTags)
}
if a.StartAtTask != "" {
args = append(args, "--start-at-task", a.StartAtTask)
}
if a.Tags != "" {
args = append(args, "--tags", a.Tags)
}
if a.PrivateKeyFile != "" {
args = append(args, "--private-key", a.PrivateKeyFile)
}
if a.User != "" {
args = append(args, "--user", a.User)
}
if a.Connection != "" {
args = append(args, "--connection", a.Connection)
}
if a.Timeout != 0 {
args = append(args, "--timeout", strconv.Itoa(a.Timeout))
}
if a.SSHCommonArgs != "" {
args = append(args, "--ssh-common-args", a.SSHCommonArgs)
}
if a.SFTPExtraArgs != "" {
args = append(args, "--sftp-extra-args", a.SFTPExtraArgs)
}
if a.SCPExtraArgs != "" {
args = append(args, "--scp-extra-args", a.SCPExtraArgs)
}
if a.SSHExtraArgs != "" {
args = append(args, "--ssh-extra-args", a.SSHExtraArgs)
}
if a.Become {
args = append(args, "--become")
}
if a.BecomeMethod != "" {
args = append(args, "--become-method", a.BecomeMethod)
}
if a.BecomeUser != "" {
args = append(args, "--become-user", a.BecomeUser)
}
if a.Verbose > 0 {
args = append(args, fmt.Sprintf("-%s", strings.Repeat("v", a.Verbose)))
}
args = append(args, a.Playbooks.Value()...)
return &types.Cmd{
Cmd: execabs.Command(ansiblePlaybookBin, args...),
Private: false,
}
}

206
ansible/ansible_test.go Normal file
View File

@ -0,0 +1,206 @@
package ansible
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/urfave/cli/v2"
)
func TestVersion(t *testing.T) {
tests := []struct {
name string
ansible *Ansible
want []string
}{
{
name: "test version command",
ansible: &Ansible{},
want: []string{ansibleBin, "--version"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.ansible.Version()
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}
func TestGalaxyInstall(t *testing.T) {
tests := []struct {
name string
ansible *Ansible
want []string
}{
{
name: "with valid requirements file and no verbosity",
ansible: &Ansible{
GalaxyRequirements: "requirements.yml",
},
want: []string{ansibleGalaxyBin, "install", "--force", "--role-file", "requirements.yml"},
},
{
name: "with valid requirements file and verbosity level 1",
ansible: &Ansible{
GalaxyRequirements: "requirements.yml",
Verbose: 1,
},
want: []string{ansibleGalaxyBin, "install", "--force", "--role-file", "requirements.yml", "-v"},
},
{
name: "with valid requirements file and verbosity level 3",
ansible: &Ansible{
GalaxyRequirements: "requirements.yml",
Verbose: 3,
},
want: []string{ansibleGalaxyBin, "install", "--force", "--role-file", "requirements.yml", "-vvv"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.ansible.GalaxyInstall()
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}
func TestAnsibleCommand(t *testing.T) {
tests := []struct {
name string
ansible *Ansible
want []string
}{
{
name: "with inventory and no other settings",
ansible: &Ansible{
Inventories: *cli.NewStringSlice("inventory.yml"),
},
want: []string{ansiblePlaybookBin, "--inventory", "inventory.yml", "--forks", "0"},
},
{
name: "with inventory and module path",
ansible: &Ansible{
Inventories: *cli.NewStringSlice("inventory.yml"),
ModulePath: *cli.NewStringSlice("/path/to/modules"),
},
want: []string{
ansiblePlaybookBin, "--inventory", "inventory.yml", "--module-path",
"/path/to/modules", "--forks", "0",
},
},
{
name: "with inventory, module path, and vault ID",
ansible: &Ansible{
Inventories: *cli.NewStringSlice("inventory.yml"),
ModulePath: *cli.NewStringSlice("/path/to/modules"),
VaultID: "my_vault_id",
},
want: []string{
ansiblePlaybookBin, "--inventory", "inventory.yml", "--module-path", "/path/to/modules",
"--vault-id", "my_vault_id", "--forks", "0",
},
},
{
name: "with inventory, module path, vault ID, and vault password file",
ansible: &Ansible{
Inventories: *cli.NewStringSlice("inventory.yml"),
ModulePath: *cli.NewStringSlice("/path/to/modules"),
VaultID: "my_vault_id",
VaultPasswordFile: "/path/to/vault/password/file",
},
want: []string{
ansiblePlaybookBin, "--inventory", "inventory.yml", "--module-path", "/path/to/modules",
"--vault-id", "my_vault_id", "--vault-password-file", "/path/to/vault/password/file",
"--forks", "0",
},
},
{
name: "with inventory, module path, vault ID, vault password file, and extra vars",
ansible: &Ansible{
Inventories: *cli.NewStringSlice("inventory.yml"),
ModulePath: *cli.NewStringSlice("/path/to/modules"),
VaultID: "my_vault_id",
VaultPasswordFile: "/path/to/vault/password/file",
ExtraVars: *cli.NewStringSlice("var1=value1", "var2=value2"),
},
want: []string{
ansiblePlaybookBin, "--inventory", "inventory.yml", "--module-path", "/path/to/modules",
"--vault-id", "my_vault_id", "--vault-password-file", "/path/to/vault/password/file",
"--extra-vars", "var1=value1", "--extra-vars", "var2=value2", "--forks", "0",
},
},
{
name: "with inventory and list hosts",
ansible: &Ansible{
ListHosts: true,
Inventories: *cli.NewStringSlice("inventory.yml"),
Playbooks: *cli.NewStringSlice("playbook1.yml", "playbook2.yml"),
},
want: []string{
ansiblePlaybookBin, "--inventory", "inventory.yml", "--list-hosts",
"playbook1.yml", "playbook2.yml",
},
},
{
name: "with inventory and syntax check",
ansible: &Ansible{
SyntaxCheck: true,
Inventories: *cli.NewStringSlice("inventory.yml"),
Playbooks: *cli.NewStringSlice("playbook1.yml", "playbook2.yml"),
},
want: []string{
ansiblePlaybookBin, "--inventory", "inventory.yml", "--syntax-check",
"playbook1.yml", "playbook2.yml",
},
},
{
name: "with all options",
ansible: &Ansible{
Check: true,
Diff: true,
FlushCache: true,
ForceHandlers: true,
Forks: 10,
Limit: "host1,host2",
ListTags: true,
ListTasks: true,
SkipTags: "tag1,tag2",
StartAtTask: "task_name",
Tags: "tag3,tag4",
PrivateKeyFile: "/path/to/private/key",
User: "remote_user",
Connection: "ssh",
Timeout: 60,
SSHCommonArgs: "-o StrictHostKeyChecking=no",
SFTPExtraArgs: "-o IdentitiesOnly=yes",
SCPExtraArgs: "-r",
SSHExtraArgs: "-o ForwardAgent=yes",
Become: true,
BecomeMethod: "sudo",
BecomeUser: "root",
Verbose: 2,
Inventories: *cli.NewStringSlice("inventory.yml"),
Playbooks: *cli.NewStringSlice("playbook1.yml", "playbook2.yml"),
},
want: []string{
ansiblePlaybookBin, "--inventory", "inventory.yml", "--check", "--diff", "--flush-cache",
"--force-handlers", "--forks", "10", "--limit", "host1,host2", "--list-tags", "--list-tasks",
"--skip-tags", "tag1,tag2", "--start-at-task", "task_name", "--tags", "tag3,tag4",
"--private-key", "/path/to/private/key", "--user", "remote_user", "--connection", "ssh",
"--timeout", "60", "--ssh-common-args", "-o StrictHostKeyChecking=no", "--sftp-extra-args",
"-o IdentitiesOnly=yes", "--scp-extra-args", "-r", "--ssh-extra-args", "-o ForwardAgent=yes",
"--become", "--become-method", "sudo", "--become-user", "root", "-vv", "playbook1.yml", "playbook2.yml",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := tt.ansible.Play()
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}

10
go.mod
View File

@ -4,8 +4,9 @@ go 1.22
require (
github.com/rs/zerolog v1.32.0
github.com/stretchr/testify v1.9.0
github.com/thegeeklab/wp-plugin-go v1.8.0
github.com/thegeeklab/wp-plugin-go/v2 v2.0.1
github.com/thegeeklab/wp-plugin-go/v2 v2.3.1
github.com/urfave/cli/v2 v2.27.2
golang.org/x/sys v0.20.0
)
@ -15,6 +16,7 @@ require (
github.com/Masterminds/semver/v3 v3.2.1 // indirect
github.com/Masterminds/sprig/v3 v3.2.3 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.1.1 // indirect
github.com/huandu/xstrings v1.3.3 // indirect
github.com/imdario/mergo v0.3.11 // indirect
@ -23,10 +25,12 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/copystructure v1.0.0 // indirect
github.com/mitchellh/reflectwalk v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/shopspring/decimal v1.2.0 // indirect
github.com/spf13/cast v1.3.1 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

13
go.sum
View File

@ -49,8 +49,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/thegeeklab/wp-plugin-go v1.8.0 h1:hmXXMRYUauSKN7QaWsQBzufzGVfrViJaDJisKh7JeKU=
github.com/thegeeklab/wp-plugin-go v1.8.0/go.mod h1:OvXWizBaHdZ77KTQKmJI6ssg33dy3eoG0t81rt5AgfY=
github.com/thegeeklab/wp-plugin-go/v2 v2.0.1 h1:42kqe5U1x5Ysa9I8tDEhh+tyvfFkfXKvlb3UsigBmN4=
github.com/thegeeklab/wp-plugin-go/v2 v2.0.1/go.mod h1:KRfDolkPSpO7Zx54Y0ofTFA7Cvd+7bHTHzYnYAo9WYg=
github.com/thegeeklab/wp-plugin-go/v2 v2.3.1 h1:ARwYgTPZ5iPsmOenmqcCf8TjiEe8wBOHKO7H/Xshe48=
github.com/thegeeklab/wp-plugin-go/v2 v2.3.1/go.mod h1:0t8M8txtEFiaB6RqLX8vLrxkqAo5FT5Hx7dztN592D4=
github.com/urfave/cli/v2 v2.27.2 h1:6e0H+AkS+zDckwPCUrZkKX38mRaau4nL2uipkJpbkcI=
github.com/urfave/cli/v2 v2.27.2/go.mod h1:g0+79LmHHATl7DAcHO99smiR/T7uGLw84w8Y42x+4eM=
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 h1:+qGGcbkzsfDQNPPe9UDgpxAWQrhbbBXOYJFQDq/dtJw=
@ -59,15 +59,15 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -92,6 +92,7 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=

View File

@ -12,7 +12,7 @@ import (
"github.com/thegeeklab/wp-ansible/plugin"
"github.com/thegeeklab/wp-plugin-go/docs"
wp_template "github.com/thegeeklab/wp-plugin-go/template"
"github.com/thegeeklab/wp-plugin-go/template"
)
func main() {
@ -23,7 +23,7 @@ func main() {
p := plugin.New(nil)
out, err := wp_template.Render(context.Background(), client, tmpl, docs.GetTemplateData(p.App))
out, err := template.Render(context.Background(), client, tmpl, docs.GetTemplateData(p.App))
if err != nil {
panic(err)
}

View File

@ -1,309 +0,0 @@
package plugin
import (
"errors"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/urfave/cli/v2"
"golang.org/x/sys/execabs"
)
const (
AnsibleForksDefault = 5
ansibleFolder = "/etc/ansible"
ansibleConfig = "/etc/ansible/ansible.cfg"
pipBin = "/usr/local/bin/pip"
ansibleBin = "/usr/local/bin/ansible"
ansibleGalaxyBin = "/usr/local/bin/ansible-galaxy"
ansiblePlaybookBin = "/usr/local/bin/ansible-playbook"
strictFilePerm = 0o600
)
const ansibleContent = `
[defaults]
host_key_checking = False
`
var ErrAnsiblePlaybookNotFound = errors.New("no playbook found")
// ansibleConfig creates the Ansible configuration directory and file.
// It ensures the directory exists and writes the Ansible configuration
// content to the config file with strict file permissions.
func (p *Plugin) ansibleConfig() error {
if err := os.MkdirAll(ansibleFolder, os.ModePerm); err != nil {
return fmt.Errorf("failed to create ansible directory: %w", err)
}
if err := os.WriteFile(ansibleConfig, []byte(ansibleContent), strictFilePerm); err != nil {
return fmt.Errorf("failed to create ansible config: %w", err)
}
return nil
}
// privateKey creates a temporary file containing the private key specified in the plugin settings,
// and sets the PrivateKeyFile field in the plugin settings to the name of the temporary file.
// This is used to pass the private key to the Ansible command.
func (p *Plugin) privateKey() error {
tmpfile, err := os.CreateTemp("", "privateKey")
if err != nil {
return fmt.Errorf("failed to create private key file: %w", err)
}
if _, err := tmpfile.Write([]byte(p.Settings.PrivateKey)); err != nil {
return fmt.Errorf("failed to write private key file: %w", err)
}
if err := tmpfile.Close(); err != nil {
return fmt.Errorf("failed to close private key file: %w", err)
}
p.Settings.PrivateKeyFile = tmpfile.Name()
return nil
}
// vaultPass creates a temporary file containing the vault password and sets the VaultPasswordFile
// field in the Plugin's Settings. This allows the vault password to be used when running
// Ansible commands that require it.
func (p *Plugin) vaultPass() error {
tmpfile, err := os.CreateTemp("", "vaultPass")
if err != nil {
return fmt.Errorf("failed to create vault password file: %w", err)
}
if _, err := tmpfile.Write([]byte(p.Settings.VaultPassword)); err != nil {
return fmt.Errorf("failed to write vault password file: %w", err)
}
if err := tmpfile.Close(); err != nil {
return fmt.Errorf("failed to close vault password file: %w", err)
}
p.Settings.VaultPasswordFile = tmpfile.Name()
return nil
}
// getPlaybooks retrieves a list of playbook files based on the configured playbook patterns.
// If any of the patterns fail to match any files, the original pattern is included in the list.
// If no playbooks are found, ErrAnsiblePlaybookNotFound is returned.
func (p *Plugin) getPlaybooks() error {
var playbooks []string
for _, pb := range p.Settings.Playbooks.Value() {
files, err := filepath.Glob(pb)
if err != nil {
playbooks = append(playbooks, pb)
continue
}
playbooks = append(playbooks, files...)
}
if len(playbooks) == 0 {
log.Debug().Strs("patterns", p.Settings.Playbooks.Value()).Msg("no playbooks found")
return ErrAnsiblePlaybookNotFound
}
p.Settings.Playbooks = *cli.NewStringSlice(playbooks...)
return nil
}
func (p *Plugin) versionCommand() *Cmd {
args := []string{
"--version",
}
return &Cmd{
Cmd: execabs.Command(ansibleBin, args...),
}
}
// pythonRequirementsCommand returns an execabs.Cmd that runs the pip install
// command with the specified Python requirements file and upgrades any existing
// packages.
func (p *Plugin) pythonRequirementsCommand() *Cmd {
args := []string{
"install",
"--upgrade",
"--requirement",
p.Settings.PythonRequirements,
}
return &Cmd{
Cmd: execabs.Command(pipBin, args...),
}
}
// galaxyRequirementsCommand returns an execabs.Cmd that runs the Ansible Galaxy
// install command with the specified role file and verbosity level.
func (p *Plugin) galaxyRequirementsCommand() *Cmd {
args := []string{
"install",
"--force",
"--role-file",
p.Settings.GalaxyRequirements,
}
if p.Settings.Verbose > 0 {
args = append(args, fmt.Sprintf("-%s", strings.Repeat("v", p.Settings.Verbose)))
}
return &Cmd{
Cmd: execabs.Command(ansibleGalaxyBin, args...),
}
}
// ansibleCommand returns an execabs.Cmd that runs the Ansible playbook with the
// specified inventory file and various configuration options set on the Plugin struct.
func (p *Plugin) ansibleCommand(inventory string) *Cmd {
args := []string{
"--inventory",
inventory,
}
if len(p.Settings.ModulePath.Value()) > 0 {
args = append(args, "--module-path", strings.Join(p.Settings.ModulePath.Value(), ":"))
}
if p.Settings.VaultID != "" {
args = append(args, "--vault-id", p.Settings.VaultID)
}
if p.Settings.VaultPasswordFile != "" {
args = append(args, "--vault-password-file", p.Settings.VaultPasswordFile)
}
for _, v := range p.Settings.ExtraVars.Value() {
args = append(args, "--extra-vars", v)
}
if p.Settings.ListHosts {
args = append(args, "--list-hosts")
args = append(args, p.Settings.Playbooks.Value()...)
return &Cmd{
Cmd: execabs.Command(ansiblePlaybookBin, args...),
}
}
if p.Settings.SyntaxCheck {
args = append(args, "--syntax-check")
args = append(args, p.Settings.Playbooks.Value()...)
return &Cmd{
Cmd: execabs.Command(ansiblePlaybookBin, args...),
}
}
if p.Settings.Check {
args = append(args, "--check")
}
if p.Settings.Diff {
args = append(args, "--diff")
}
if p.Settings.FlushCache {
args = append(args, "--flush-cache")
}
if p.Settings.ForceHandlers {
args = append(args, "--force-handlers")
}
if p.Settings.Forks != AnsibleForksDefault {
args = append(args, "--forks", strconv.Itoa(p.Settings.Forks))
}
if p.Settings.Limit != "" {
args = append(args, "--limit", p.Settings.Limit)
}
if p.Settings.ListTags {
args = append(args, "--list-tags")
}
if p.Settings.ListTasks {
args = append(args, "--list-tasks")
}
if p.Settings.SkipTags != "" {
args = append(args, "--skip-tags", p.Settings.SkipTags)
}
if p.Settings.StartAtTask != "" {
args = append(args, "--start-at-task", p.Settings.StartAtTask)
}
if p.Settings.Tags != "" {
args = append(args, "--tags", p.Settings.Tags)
}
if p.Settings.PrivateKeyFile != "" {
args = append(args, "--private-key", p.Settings.PrivateKeyFile)
}
if p.Settings.User != "" {
args = append(args, "--user", p.Settings.User)
}
if p.Settings.Connection != "" {
args = append(args, "--connection", p.Settings.Connection)
}
if p.Settings.Timeout != 0 {
args = append(args, "--timeout", strconv.Itoa(p.Settings.Timeout))
}
if p.Settings.SSHCommonArgs != "" {
args = append(args, "--ssh-common-args", p.Settings.SSHCommonArgs)
}
if p.Settings.SFTPExtraArgs != "" {
args = append(args, "--sftp-extra-args", p.Settings.SFTPExtraArgs)
}
if p.Settings.SCPExtraArgs != "" {
args = append(args, "--scp-extra-args", p.Settings.SCPExtraArgs)
}
if p.Settings.SSHExtraArgs != "" {
args = append(args, "--ssh-extra-args", p.Settings.SSHExtraArgs)
}
if p.Settings.Become {
args = append(args, "--become")
}
if p.Settings.BecomeMethod != "" {
args = append(args, "--become-method", p.Settings.BecomeMethod)
}
if p.Settings.BecomeUser != "" {
args = append(args, "--become-user", p.Settings.BecomeUser)
}
if p.Settings.Verbose > 0 {
args = append(args, fmt.Sprintf("-%s", strings.Repeat("v", p.Settings.Verbose)))
}
args = append(args, p.Settings.Playbooks.Value()...)
return &Cmd{
Cmd: execabs.Command(ansiblePlaybookBin, args...),
Private: false,
}
}

View File

@ -5,7 +5,8 @@ import (
"fmt"
"os"
"github.com/thegeeklab/wp-plugin-go/v2/trace"
"github.com/thegeeklab/wp-plugin-go/v2/file"
"github.com/thegeeklab/wp-plugin-go/v2/types"
)
func (p *Plugin) run(_ context.Context) error {
@ -22,59 +23,51 @@ func (p *Plugin) run(_ context.Context) error {
// Validate handles the settings validation of the plugin.
func (p *Plugin) Validate() error {
if err := p.Settings.Ansible.GetPlaybooks(); err != nil {
return err
}
return nil
}
// Execute provides the implementation of the plugin.
func (p *Plugin) Execute() error {
batchCmd := make([]*Cmd, 0)
batchCmd = append(batchCmd, p.versionCommand())
var err error
if err := p.getPlaybooks(); err != nil {
return err
}
batchCmd := make([]*types.Cmd, 0)
if err := p.ansibleConfig(); err != nil {
return err
}
batchCmd = append(batchCmd, p.Settings.Ansible.Version())
if p.Settings.PrivateKey != "" {
if err := p.privateKey(); err != nil {
if p.Settings.Ansible.PrivateKeyFile, err = file.WriteTmpFile("privateKey", p.Settings.PrivateKey); err != nil {
return err
}
defer os.Remove(p.Settings.PrivateKeyFile)
defer os.Remove(p.Settings.Ansible.PrivateKeyFile)
}
if p.Settings.VaultPassword != "" {
if err := p.vaultPass(); err != nil {
if p.Settings.Ansible.VaultPasswordFile, err = file.WriteTmpFile("vaultPass", p.Settings.VaultPassword); err != nil {
return err
}
defer os.Remove(p.Settings.VaultPasswordFile)
defer os.Remove(p.Settings.Ansible.VaultPasswordFile)
}
if p.Settings.PythonRequirements != "" {
batchCmd = append(batchCmd, p.pythonRequirementsCommand())
batchCmd = append(batchCmd, PipInstall(p.Settings.PythonRequirements))
}
if p.Settings.GalaxyRequirements != "" {
batchCmd = append(batchCmd, p.galaxyRequirementsCommand())
if p.Settings.Ansible.GalaxyRequirements != "" {
batchCmd = append(batchCmd, p.Settings.Ansible.GalaxyInstall())
}
for _, inventory := range p.Settings.Inventories.Value() {
batchCmd = append(batchCmd, p.ansibleCommand(inventory))
}
batchCmd = append(batchCmd, p.Settings.Ansible.Play())
for _, bc := range batchCmd {
bc.Stdout = os.Stdout
bc.Stderr = os.Stderr
trace.Cmd(bc.Cmd)
for _, cmd := range batchCmd {
cmd.Env = append(os.Environ(), "ANSIBLE_FORCE_COLOR=1")
bc.Env = os.Environ()
bc.Env = append(bc.Env, "ANSIBLE_FORCE_COLOR=1")
if err := bc.Run(); err != nil {
if err := cmd.Run(); err != nil {
return err
}
}

View File

@ -3,9 +3,9 @@ package plugin
import (
"fmt"
"github.com/thegeeklab/wp-ansible/ansible"
wp "github.com/thegeeklab/wp-plugin-go/v2/plugin"
"github.com/urfave/cli/v2"
"golang.org/x/sys/execabs"
)
//go:generate go run ../internal/docs/main.go -output=../docs/data/data-raw.yaml
@ -19,45 +19,9 @@ type Plugin struct {
// Settings for the Plugin.
type Settings struct {
PythonRequirements string
GalaxyRequirements string
Inventories cli.StringSlice
Playbooks cli.StringSlice
Limit string
SkipTags string
StartAtTask string
Tags string
ExtraVars cli.StringSlice
ModulePath cli.StringSlice
Check bool
Diff bool
FlushCache bool
ForceHandlers bool
ListHosts bool
ListTags bool
ListTasks bool
SyntaxCheck bool
Forks int
VaultID string
VaultPassword string
VaultPasswordFile string
Verbose int
PrivateKey string
PrivateKeyFile string
User string
Connection string
Timeout int
SSHCommonArgs string
SFTPExtraArgs string
SCPExtraArgs string
SSHExtraArgs string
Become bool
BecomeMethod string
BecomeUser string
}
type Cmd struct {
*execabs.Cmd
Private bool
VaultPassword string
Ansible ansible.Ansible
}
func New(e wp.ExecuteFunc, build ...string) *Plugin {
@ -104,7 +68,7 @@ func Flags(settings *Settings, category string) []cli.Flag {
Name: "galaxy-requirements",
Usage: "path to galaxy requirements file",
EnvVars: []string{"PLUGIN_GALAXY_REQUIREMENTS"},
Destination: &settings.GalaxyRequirements,
Destination: &settings.Ansible.GalaxyRequirements,
Category: category,
},
&cli.StringSliceFlag{
@ -112,7 +76,7 @@ func Flags(settings *Settings, category string) []cli.Flag {
Usage: "path to inventory file",
EnvVars: []string{"PLUGIN_INVENTORY", "PLUGIN_INVENTORIES"},
Required: true,
Destination: &settings.Inventories,
Destination: &settings.Ansible.Inventories,
Category: category,
},
&cli.StringSliceFlag{
@ -120,120 +84,120 @@ func Flags(settings *Settings, category string) []cli.Flag {
Usage: "list of playbooks to apply",
EnvVars: []string{"PLUGIN_PLAYBOOK", "PLUGIN_PLAYBOOKS"},
Required: true,
Destination: &settings.Playbooks,
Destination: &settings.Ansible.Playbooks,
Category: category,
},
&cli.StringFlag{
Name: "limit",
Usage: "limit selected hosts to an additional pattern",
EnvVars: []string{"PLUGIN_LIMIT"},
Destination: &settings.Limit,
Destination: &settings.Ansible.Limit,
Category: category,
},
&cli.StringFlag{
Name: "skip-tags",
Usage: "only run plays and tasks whose tags do not match",
EnvVars: []string{"PLUGIN_SKIP_TAGS"},
Destination: &settings.SkipTags,
Destination: &settings.Ansible.SkipTags,
Category: category,
},
&cli.StringFlag{
Name: "start-at-task",
Usage: "start the playbook at the task matching this name",
EnvVars: []string{"PLUGIN_START_AT_TASK"},
Destination: &settings.StartAtTask,
Destination: &settings.Ansible.StartAtTask,
Category: category,
},
&cli.StringFlag{
Name: "tags",
Usage: "only run plays and tasks tagged with these values",
EnvVars: []string{"PLUGIN_TAGS"},
Destination: &settings.Tags,
Destination: &settings.Ansible.Tags,
Category: category,
},
&cli.StringSliceFlag{
Name: "extra-vars",
Usage: "set additional variables as `key=value`",
EnvVars: []string{"PLUGIN_EXTRA_VARS", "ANSIBLE_EXTRA_VARS"},
Destination: &settings.ExtraVars,
Destination: &settings.Ansible.ExtraVars,
Category: category,
},
&cli.StringSliceFlag{
Name: "module-path",
Usage: "prepend paths to module library",
EnvVars: []string{"PLUGIN_MODULE_PATH"},
Destination: &settings.ModulePath,
Destination: &settings.Ansible.ModulePath,
Category: category,
},
&cli.BoolFlag{
Name: "check",
Usage: "run a check, do not apply any changes",
EnvVars: []string{"PLUGIN_CHECK"},
Destination: &settings.Check,
Destination: &settings.Ansible.Check,
Category: category,
},
&cli.BoolFlag{
Name: "diff",
Usage: "show the differences, may print secrets",
EnvVars: []string{"PLUGIN_DIFF"},
Destination: &settings.Diff,
Destination: &settings.Ansible.Diff,
Category: category,
},
&cli.BoolFlag{
Name: "flush-cache",
Usage: "clear the fact cache for every host in inventory",
EnvVars: []string{"PLUGIN_FLUSH_CACHE"},
Destination: &settings.FlushCache,
Destination: &settings.Ansible.FlushCache,
Category: category,
},
&cli.BoolFlag{
Name: "force-handlers",
Usage: "run handlers even if a task fails",
EnvVars: []string{"PLUGIN_FORCE_HANDLERS"},
Destination: &settings.ForceHandlers,
Destination: &settings.Ansible.ForceHandlers,
Category: category,
},
&cli.BoolFlag{
Name: "list-hosts",
Usage: "outputs a list of matching hosts",
EnvVars: []string{"PLUGIN_LIST_HOSTS"},
Destination: &settings.ListHosts,
Destination: &settings.Ansible.ListHosts,
Category: category,
},
&cli.BoolFlag{
Name: "list-tags",
Usage: "list all available tags",
EnvVars: []string{"PLUGIN_LIST_TAGS"},
Destination: &settings.ListTags,
Destination: &settings.Ansible.ListTags,
Category: category,
},
&cli.BoolFlag{
Name: "list-tasks",
Usage: "list all tasks that would be executed",
EnvVars: []string{"PLUGIN_LIST_TASKS"},
Destination: &settings.ListTasks,
Destination: &settings.Ansible.ListTasks,
Category: category,
},
&cli.BoolFlag{
Name: "syntax-check",
Usage: "perform a syntax check on the playbook",
EnvVars: []string{"PLUGIN_SYNTAX_CHECK"},
Destination: &settings.SyntaxCheck,
Destination: &settings.Ansible.SyntaxCheck,
Category: category,
},
&cli.IntFlag{
Name: "forks",
Usage: "specify number of parallel processes to use",
EnvVars: []string{"PLUGIN_FORKS"},
Value: AnsibleForksDefault,
Destination: &settings.Forks,
Value: ansible.AnsibleForksDefault,
Destination: &settings.Ansible.Forks,
Category: category,
},
&cli.StringFlag{
Name: "vault-id",
Usage: "the vault identity to use",
EnvVars: []string{"PLUGIN_VAULT_ID", "ANSIBLE_VAULT_ID"},
Destination: &settings.VaultID,
Destination: &settings.Ansible.VaultID,
Category: category,
},
&cli.StringFlag{
@ -247,7 +211,7 @@ func Flags(settings *Settings, category string) []cli.Flag {
Name: "verbose",
Usage: "level of verbosity, 0 up to 4",
EnvVars: []string{"PLUGIN_VERBOSE"},
Destination: &settings.Verbose,
Destination: &settings.Ansible.Verbose,
Category: category,
},
&cli.StringFlag{
@ -261,70 +225,70 @@ func Flags(settings *Settings, category string) []cli.Flag {
Name: "user",
Usage: "connect as this user",
EnvVars: []string{"PLUGIN_USER", "ANSIBLE_USER"},
Destination: &settings.User,
Destination: &settings.Ansible.User,
Category: category,
},
&cli.StringFlag{
Name: "connection",
Usage: "connection type to use",
EnvVars: []string{"PLUGIN_CONNECTION"},
Destination: &settings.Connection,
Destination: &settings.Ansible.Connection,
Category: category,
},
&cli.IntFlag{
Name: "timeout",
Usage: "override the connection timeout in seconds",
EnvVars: []string{"PLUGIN_TIMEOUT"},
Destination: &settings.Timeout,
Destination: &settings.Ansible.Timeout,
Category: category,
},
&cli.StringFlag{
Name: "ssh-common-args",
Usage: "specify common arguments to pass to SFTP, SCP and SSH connections",
EnvVars: []string{"PLUGIN_SSH_COMMON_ARGS"},
Destination: &settings.SSHCommonArgs,
Destination: &settings.Ansible.SSHCommonArgs,
Category: category,
},
&cli.StringFlag{
Name: "sftp-extra-args",
Usage: "specify extra arguments to pass to SFTP connections only",
EnvVars: []string{"PLUGIN_SFTP_EXTRA_ARGS"},
Destination: &settings.SFTPExtraArgs,
Destination: &settings.Ansible.SFTPExtraArgs,
Category: category,
},
&cli.StringFlag{
Name: "scp-extra-args",
Usage: "specify extra arguments to pass to SCP connections only",
EnvVars: []string{"PLUGIN_SCP_EXTRA_ARGS"},
Destination: &settings.SCPExtraArgs,
Destination: &settings.Ansible.SCPExtraArgs,
Category: category,
},
&cli.StringFlag{
Name: "ssh-extra-args",
Usage: "specify extra arguments to pass to SSH connections only",
EnvVars: []string{"PLUGIN_SSH_EXTRA_ARGS"},
Destination: &settings.SSHExtraArgs,
Destination: &settings.Ansible.SSHExtraArgs,
Category: category,
},
&cli.BoolFlag{
Name: "become",
Usage: "enable privilege escalation",
EnvVars: []string{"PLUGIN_BECOME"},
Destination: &settings.Become,
Destination: &settings.Ansible.Become,
Category: category,
},
&cli.StringFlag{
Name: "become-method",
Usage: "privilege escalation method to use",
EnvVars: []string{"PLUGIN_BECOME_METHOD", "ANSIBLE_BECOME_METHOD"},
Destination: &settings.BecomeMethod,
Destination: &settings.Ansible.BecomeMethod,
Category: category,
},
&cli.StringFlag{
Name: "become-user",
Usage: "privilege escalation user to use",
EnvVars: []string{"PLUGIN_BECOME_USER", "ANSIBLE_BECOME_USER"},
Destination: &settings.BecomeUser,
Destination: &settings.Ansible.BecomeUser,
Category: category,
},
}

23
plugin/util.go Normal file
View File

@ -0,0 +1,23 @@
package plugin
import (
"github.com/thegeeklab/wp-plugin-go/v2/types"
"golang.org/x/sys/execabs"
)
const pipBin = "/usr/local/bin/pip"
// PipInstall returns a command to install Python packages from a requirements file.
// The command will upgrade any existing packages and install the packages specified in the given requirements file.
func PipInstall(req string) *types.Cmd {
args := []string{
"install",
"--upgrade",
"--requirement",
req,
}
return &types.Cmd{
Cmd: execabs.Command(pipBin, args...),
}
}

28
plugin/util_test.go Normal file
View File

@ -0,0 +1,28 @@
package plugin
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestPipInstall(t *testing.T) {
tests := []struct {
name string
requirements string
want []string
}{
{
name: "with valid requirements file",
requirements: "requirements.txt",
want: []string{pipBin, "install", "--upgrade", "--requirement", "requirements.txt"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
cmd := PipInstall(tt.requirements)
assert.Equal(t, tt.want, cmd.Cmd.Args)
})
}
}