From bb93d6540d16f34984d5462211aa6d9d7bc51540 Mon Sep 17 00:00:00 2001 From: Arun Babu Neelicattu Date: Sun, 1 Apr 2018 14:41:17 +1200 Subject: [PATCH] Data Source: local_exec Add new data source `local_exec`. This allows for the execution of a specified command on the local system and retrieve it's output. Resolves: #8 --- internal/provider/data_source_local_exec.go | 119 ++++++++++++++++++ .../data_source_local_exec_linux_test.go | 80 ++++++++++++ internal/provider/provider.go | 1 + website/docs/d/exec.html.md | 52 ++++++++ 4 files changed, 252 insertions(+) create mode 100644 internal/provider/data_source_local_exec.go create mode 100644 internal/provider/data_source_local_exec_linux_test.go create mode 100644 website/docs/d/exec.html.md diff --git a/internal/provider/data_source_local_exec.go b/internal/provider/data_source_local_exec.go new file mode 100644 index 00000000..49b476a3 --- /dev/null +++ b/internal/provider/data_source_local_exec.go @@ -0,0 +1,119 @@ +package provider + +import ( + "crypto/sha1" + "encoding/hex" + "fmt" + "io/ioutil" + "os/exec" + "syscall" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func dataSourceLocalExec() *schema.Resource { + return &schema.Resource{ + Read: dataSourceLocalExecRead, + + Schema: map[string]*schema.Schema{ + "command": { + Type: schema.TypeList, + Description: "Command to execute", + Required: true, + ForceNew: true, + Elem: &schema.Schema{Type: schema.TypeString}, + }, + "working_dir": { + Type: schema.TypeString, + Description: "Directory to change into before executing provided command", + Optional: true, + Default: "", + ForceNew: true, + }, + "ignore_failure": { + Type: schema.TypeBool, + Description: "If set to true, command execution failures will be ignored", + Optional: true, + Default: false, + ForceNew: true, + }, + "stdout": { + Type: schema.TypeString, + Computed: true, + }, + "stderr": { + Type: schema.TypeString, + Computed: true, + }, + "rc": { + Type: schema.TypeInt, + Computed: true, + }, + }, + } +} + +func dataSourceLocalExecRead(d *schema.ResourceData, _ interface{}) error { + exitCode := 0 + command, args, _ := expandCommand(d) + ignoreFailure := d.Get("ignore_failure").(bool) + + cmd := exec.Command(command, args...) + cmd.Dir = d.Get("working_dir").(string) + + stdoutPipe, err := cmd.StdoutPipe() + if err != nil { + return err + } + + stderrPipe, err := cmd.StderrPipe() + if err != nil { + return err + } + + if err := cmd.Start(); err != nil { + return err + } + + stderr, _ := ioutil.ReadAll(stderrPipe) + stdout, _ := ioutil.ReadAll(stdoutPipe) + + if err := cmd.Wait(); err != nil { + if exitError, ok := err.(*exec.ExitError); ok { + if status, ok := exitError.Sys().(syscall.WaitStatus); ok { + exitCode = status.ExitStatus() + } else { + // unable to retrieve exit code from error, use default + exitCode = -1 + } + } + + if !ignoreFailure { + return err + } + } + + d.Set("stderr", string(stderr)) + d.Set("stdout", string(stdout)) + d.Set("rc", exitCode) + + // use the checksum of (stdout, stderr, rc) to generate id + checksum := sha1.Sum( + append([]byte(stdout), + append([]byte(stderr), + []byte(fmt.Sprintf("%d", exitCode))...)...)) + d.SetId(hex.EncodeToString(checksum[:])) + + return nil +} + +func expandCommand(d *schema.ResourceData) (string, []string, error) { + execCommand := d.Get("command").([]interface{}) + command := make([]string, 0) + + for _, commandRaw := range execCommand { + command = append(command, commandRaw.(string)) + } + + return command[0], command[1:], nil +} diff --git a/internal/provider/data_source_local_exec_linux_test.go b/internal/provider/data_source_local_exec_linux_test.go new file mode 100644 index 00000000..350b995f --- /dev/null +++ b/internal/provider/data_source_local_exec_linux_test.go @@ -0,0 +1,80 @@ +// +build linux + +package provider + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" +) + +func TestLocalExecDataSource(t *testing.T) { + var tests = []struct { + stdout string + stderr string + rc string + config string + }{ + { + "hello", + "world", + "0", + ` + data "local_exec" "command" { + command = ["sh", "-c", "echo -n hello; echo -n 1>&2 world"] + } + `, + }, + { + "", + "", + "127", + ` + data "local_exec" "command" { + command = ["sh", "-c", "exit 127"] + ignore_failure = true + } + `, + }, + { + "/tmp\n", + "", + "0", + ` + data "local_exec" "command" { + command = ["pwd"] + working_dir = "/tmp" + } + `, + }, + } + + for _, test := range tests { + t.Run("", func(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + Providers: testProviders, + Steps: []resource.TestStep{ + { + Config: test.config, + Check: func(s *terraform.State) error { + m := s.RootModule() + i := m.Resources["data.local_exec.command"].Primary + if got, want := i.Attributes["stdout"], test.stdout; got != want { + return fmt.Errorf("stdout %q; want %q", got, want) + } + if got, want := i.Attributes["stderr"], test.stderr; got != want { + return fmt.Errorf("stderr %q; want %q", got, want) + } + if got, want := i.Attributes["rc"], test.rc; got != want { + return fmt.Errorf("rc %q; want %q", got, want) + } + return nil + }, + }, + }, + }) + }) + } +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index f2f8722b..b000124c 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -11,6 +11,7 @@ func New() *schema.Provider { "local_file": resourceLocalFile(), }, DataSourcesMap: map[string]*schema.Resource{ + "local_exec": dataSourceLocalExec(), "local_file": dataSourceLocalFile(), }, } diff --git a/website/docs/d/exec.html.md b/website/docs/d/exec.html.md new file mode 100644 index 00000000..6d21c986 --- /dev/null +++ b/website/docs/d/exec.html.md @@ -0,0 +1,52 @@ +--- +layout: "local" +page_title: "Local: local_exec" +sidebar_current: "docs-local-datasource-exec" +description: |- + Executes a command on the local system and returns stdout, stderr and rc. +--- + +# local_exec + +`local_exec` executes a command on the local system. + +## Example Usage + +```hcl +data "local_exec" "touch" { + command = ["touch", "bar"] + working_dir = "/tmp" +} + +data "local_exec" "sh" { + command = ["sh", "-c", "echo hello world && curl -L https://google.com"] + ignore_failure = true +} + +``` + +## Argument Reference + +The following arguments are supported: + +* `command` - (Required) Command and arguments to execute. This is expected as + a list with the first element being the the binary to execute. The rest will + be passed as arguments to the binary on execution. The binary should be + available in the `PATH` or should be an absolute path. +* `working_dir` - (Optional) The directory to change to before executing the + specified command. If unspecified, the process's current directory will be + used. +* `ignore_failure` - (Optional) By default, any failures during the execution + of the command will cause an error in your Terraform execution. If an error + is expected or not fatal, this may be set to `true` to ignore any such + failures. + +## Attributes Exported + +The following attributes are exported: + +* `stdout` - The raw content of stdout of the process executing the command. +* `stderr` - The raw content of stderr of the process executing the command. +* `rc` - The exit code of the process executing the command. On success, this + is always 0. On failure, this retrieved at best effort and defaults to `-1` + if it cannot be retrieved.