Skip to content
Open
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
119 changes: 119 additions & 0 deletions internal/provider/data_source_local_exec.go
Original file line number Diff line number Diff line change
@@ -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
}
80 changes: 80 additions & 0 deletions internal/provider/data_source_local_exec_linux_test.go
Original file line number Diff line number Diff line change
@@ -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
},
},
},
})
})
}
}
1 change: 1 addition & 0 deletions internal/provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ func New() *schema.Provider {
"local_file": resourceLocalFile(),
},
DataSourcesMap: map[string]*schema.Resource{
"local_exec": dataSourceLocalExec(),
"local_file": dataSourceLocalFile(),
},
}
Expand Down
52 changes: 52 additions & 0 deletions website/docs/d/exec.html.md
Original file line number Diff line number Diff line change
@@ -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.