From 3148862ffaf66fa8402f0210ce01b026edfdedfe Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Mon, 6 May 2024 20:29:44 +0200 Subject: [PATCH] refactor: switch to plugin Cmd and add tests (#58) --- ansible/ansible.go | 258 +++++++++++++++++++++++++++++++++ ansible/ansible_test.go | 206 +++++++++++++++++++++++++++ go.mod | 6 +- go.sum | 5 +- plugin/ansible.go | 309 ---------------------------------------- plugin/impl.go | 47 +++--- plugin/plugin.go | 104 +++++--------- plugin/util.go | 23 +++ plugin/util_test.go | 28 ++++ 9 files changed, 577 insertions(+), 409 deletions(-) create mode 100644 ansible/ansible.go create mode 100644 ansible/ansible_test.go delete mode 100644 plugin/ansible.go create mode 100644 plugin/util.go create mode 100644 plugin/util_test.go diff --git a/ansible/ansible.go b/ansible/ansible.go new file mode 100644 index 0000000..f7d411e --- /dev/null +++ b/ansible/ansible.go @@ -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, + } +} diff --git a/ansible/ansible_test.go b/ansible/ansible_test.go new file mode 100644 index 0000000..d1dc4fb --- /dev/null +++ b/ansible/ansible_test.go @@ -0,0 +1,206 @@ +package ansible + +import ( + "testing" + + "github.com/stretchr/testify/require" + "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() + require.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() + require.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() + require.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +} diff --git a/go.mod b/go.mod index d1a1d66..09118a8 100644 --- a/go.mod +++ b/go.mod @@ -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.0 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 + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 84461b3..4c9dac7 100644 --- a/go.sum +++ b/go.sum @@ -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.0 h1:9LOdITzjxEEbgcH9yU0tDZL0dcDZzNYzbk2+hZ5QsBA= +github.com/thegeeklab/wp-plugin-go/v2 v2.3.0/go.mod h1:I/3M/4OPvr4FFS+s0aaImpX1llA/lS2KC6Bnp+qzsCs= 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= @@ -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= diff --git a/plugin/ansible.go b/plugin/ansible.go deleted file mode 100644 index 8699862..0000000 --- a/plugin/ansible.go +++ /dev/null @@ -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, - } -} diff --git a/plugin/impl.go b/plugin/impl.go index 508e44c..cc12144 100644 --- a/plugin/impl.go +++ b/plugin/impl.go @@ -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()) - } - - if p.Settings.GalaxyRequirements != "" { - batchCmd = append(batchCmd, p.galaxyRequirementsCommand()) + batchCmd = append(batchCmd, PipInstall(p.Settings.PythonRequirements)) } - for _, inventory := range p.Settings.Inventories.Value() { - batchCmd = append(batchCmd, p.ansibleCommand(inventory)) + if p.Settings.Ansible.GalaxyRequirements != "" { + batchCmd = append(batchCmd, p.Settings.Ansible.GalaxyInstall()) } - for _, bc := range batchCmd { - bc.Stdout = os.Stdout - bc.Stderr = os.Stderr - trace.Cmd(bc.Cmd) + batchCmd = append(batchCmd, p.Settings.Ansible.Play()) - bc.Env = os.Environ() - bc.Env = append(bc.Env, "ANSIBLE_FORCE_COLOR=1") + for _, cmd := range batchCmd { + cmd.Env = append(os.Environ(), "ANSIBLE_FORCE_COLOR=1") - if err := bc.Run(); err != nil { + if err := cmd.Run(); err != nil { return err } } diff --git a/plugin/plugin.go b/plugin/plugin.go index 79d6cac..001ad6d 100644 --- a/plugin/plugin.go +++ b/plugin/plugin.go @@ -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, }, } diff --git a/plugin/util.go b/plugin/util.go new file mode 100644 index 0000000..510c032 --- /dev/null +++ b/plugin/util.go @@ -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...), + } +} diff --git a/plugin/util_test.go b/plugin/util_test.go new file mode 100644 index 0000000..5c9b659 --- /dev/null +++ b/plugin/util_test.go @@ -0,0 +1,28 @@ +package plugin + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +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) + require.Equal(t, tt.want, cmd.Cmd.Args) + }) + } +}