Skip to content

Commit bb93d65

Browse files
committed
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
1 parent b9c4652 commit bb93d65

File tree

4 files changed

+252
-0
lines changed

4 files changed

+252
-0
lines changed
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package provider
2+
3+
import (
4+
"crypto/sha1"
5+
"encoding/hex"
6+
"fmt"
7+
"io/ioutil"
8+
"os/exec"
9+
"syscall"
10+
11+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
12+
)
13+
14+
func dataSourceLocalExec() *schema.Resource {
15+
return &schema.Resource{
16+
Read: dataSourceLocalExecRead,
17+
18+
Schema: map[string]*schema.Schema{
19+
"command": {
20+
Type: schema.TypeList,
21+
Description: "Command to execute",
22+
Required: true,
23+
ForceNew: true,
24+
Elem: &schema.Schema{Type: schema.TypeString},
25+
},
26+
"working_dir": {
27+
Type: schema.TypeString,
28+
Description: "Directory to change into before executing provided command",
29+
Optional: true,
30+
Default: "",
31+
ForceNew: true,
32+
},
33+
"ignore_failure": {
34+
Type: schema.TypeBool,
35+
Description: "If set to true, command execution failures will be ignored",
36+
Optional: true,
37+
Default: false,
38+
ForceNew: true,
39+
},
40+
"stdout": {
41+
Type: schema.TypeString,
42+
Computed: true,
43+
},
44+
"stderr": {
45+
Type: schema.TypeString,
46+
Computed: true,
47+
},
48+
"rc": {
49+
Type: schema.TypeInt,
50+
Computed: true,
51+
},
52+
},
53+
}
54+
}
55+
56+
func dataSourceLocalExecRead(d *schema.ResourceData, _ interface{}) error {
57+
exitCode := 0
58+
command, args, _ := expandCommand(d)
59+
ignoreFailure := d.Get("ignore_failure").(bool)
60+
61+
cmd := exec.Command(command, args...)
62+
cmd.Dir = d.Get("working_dir").(string)
63+
64+
stdoutPipe, err := cmd.StdoutPipe()
65+
if err != nil {
66+
return err
67+
}
68+
69+
stderrPipe, err := cmd.StderrPipe()
70+
if err != nil {
71+
return err
72+
}
73+
74+
if err := cmd.Start(); err != nil {
75+
return err
76+
}
77+
78+
stderr, _ := ioutil.ReadAll(stderrPipe)
79+
stdout, _ := ioutil.ReadAll(stdoutPipe)
80+
81+
if err := cmd.Wait(); err != nil {
82+
if exitError, ok := err.(*exec.ExitError); ok {
83+
if status, ok := exitError.Sys().(syscall.WaitStatus); ok {
84+
exitCode = status.ExitStatus()
85+
} else {
86+
// unable to retrieve exit code from error, use default
87+
exitCode = -1
88+
}
89+
}
90+
91+
if !ignoreFailure {
92+
return err
93+
}
94+
}
95+
96+
d.Set("stderr", string(stderr))
97+
d.Set("stdout", string(stdout))
98+
d.Set("rc", exitCode)
99+
100+
// use the checksum of (stdout, stderr, rc) to generate id
101+
checksum := sha1.Sum(
102+
append([]byte(stdout),
103+
append([]byte(stderr),
104+
[]byte(fmt.Sprintf("%d", exitCode))...)...))
105+
d.SetId(hex.EncodeToString(checksum[:]))
106+
107+
return nil
108+
}
109+
110+
func expandCommand(d *schema.ResourceData) (string, []string, error) {
111+
execCommand := d.Get("command").([]interface{})
112+
command := make([]string, 0)
113+
114+
for _, commandRaw := range execCommand {
115+
command = append(command, commandRaw.(string))
116+
}
117+
118+
return command[0], command[1:], nil
119+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// +build linux
2+
3+
package provider
4+
5+
import (
6+
"fmt"
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
10+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
11+
)
12+
13+
func TestLocalExecDataSource(t *testing.T) {
14+
var tests = []struct {
15+
stdout string
16+
stderr string
17+
rc string
18+
config string
19+
}{
20+
{
21+
"hello",
22+
"world",
23+
"0",
24+
`
25+
data "local_exec" "command" {
26+
command = ["sh", "-c", "echo -n hello; echo -n 1>&2 world"]
27+
}
28+
`,
29+
},
30+
{
31+
"",
32+
"",
33+
"127",
34+
`
35+
data "local_exec" "command" {
36+
command = ["sh", "-c", "exit 127"]
37+
ignore_failure = true
38+
}
39+
`,
40+
},
41+
{
42+
"/tmp\n",
43+
"",
44+
"0",
45+
`
46+
data "local_exec" "command" {
47+
command = ["pwd"]
48+
working_dir = "/tmp"
49+
}
50+
`,
51+
},
52+
}
53+
54+
for _, test := range tests {
55+
t.Run("", func(t *testing.T) {
56+
resource.UnitTest(t, resource.TestCase{
57+
Providers: testProviders,
58+
Steps: []resource.TestStep{
59+
{
60+
Config: test.config,
61+
Check: func(s *terraform.State) error {
62+
m := s.RootModule()
63+
i := m.Resources["data.local_exec.command"].Primary
64+
if got, want := i.Attributes["stdout"], test.stdout; got != want {
65+
return fmt.Errorf("stdout %q; want %q", got, want)
66+
}
67+
if got, want := i.Attributes["stderr"], test.stderr; got != want {
68+
return fmt.Errorf("stderr %q; want %q", got, want)
69+
}
70+
if got, want := i.Attributes["rc"], test.rc; got != want {
71+
return fmt.Errorf("rc %q; want %q", got, want)
72+
}
73+
return nil
74+
},
75+
},
76+
},
77+
})
78+
})
79+
}
80+
}

internal/provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ func New() *schema.Provider {
1111
"local_file": resourceLocalFile(),
1212
},
1313
DataSourcesMap: map[string]*schema.Resource{
14+
"local_exec": dataSourceLocalExec(),
1415
"local_file": dataSourceLocalFile(),
1516
},
1617
}

website/docs/d/exec.html.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
---
2+
layout: "local"
3+
page_title: "Local: local_exec"
4+
sidebar_current: "docs-local-datasource-exec"
5+
description: |-
6+
Executes a command on the local system and returns stdout, stderr and rc.
7+
---
8+
9+
# local_exec
10+
11+
`local_exec` executes a command on the local system.
12+
13+
## Example Usage
14+
15+
```hcl
16+
data "local_exec" "touch" {
17+
command = ["touch", "bar"]
18+
working_dir = "/tmp"
19+
}
20+
21+
data "local_exec" "sh" {
22+
command = ["sh", "-c", "echo hello world && curl -L https://google.com"]
23+
ignore_failure = true
24+
}
25+
26+
```
27+
28+
## Argument Reference
29+
30+
The following arguments are supported:
31+
32+
* `command` - (Required) Command and arguments to execute. This is expected as
33+
a list with the first element being the the binary to execute. The rest will
34+
be passed as arguments to the binary on execution. The binary should be
35+
available in the `PATH` or should be an absolute path.
36+
* `working_dir` - (Optional) The directory to change to before executing the
37+
specified command. If unspecified, the process's current directory will be
38+
used.
39+
* `ignore_failure` - (Optional) By default, any failures during the execution
40+
of the command will cause an error in your Terraform execution. If an error
41+
is expected or not fatal, this may be set to `true` to ignore any such
42+
failures.
43+
44+
## Attributes Exported
45+
46+
The following attributes are exported:
47+
48+
* `stdout` - The raw content of stdout of the process executing the command.
49+
* `stderr` - The raw content of stderr of the process executing the command.
50+
* `rc` - The exit code of the process executing the command. On success, this
51+
is always 0. On failure, this retrieved at best effort and defaults to `-1`
52+
if it cannot be retrieved.

0 commit comments

Comments
 (0)