-
Notifications
You must be signed in to change notification settings - Fork 9.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
A data source that executes a child process, expecting it to support a particular gateway protocol, and exports its result. This can be used as a straightforward way to retrieve data from sources that Terraform doesn't natively support..
- Loading branch information
1 parent
1ddb283
commit d2fb4d2
Showing
5 changed files
with
304 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,93 @@ | ||
package external | ||
|
||
import ( | ||
"bytes" | ||
"encoding/json" | ||
"fmt" | ||
"os/exec" | ||
|
||
"github.com/hashicorp/terraform/helper/schema" | ||
) | ||
|
||
func dataSource() *schema.Resource { | ||
return &schema.Resource{ | ||
Read: dataSourceRead, | ||
|
||
Schema: map[string]*schema.Schema{ | ||
"program": &schema.Schema{ | ||
Type: schema.TypeList, | ||
Required: true, | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
}, | ||
|
||
"query": &schema.Schema{ | ||
Type: schema.TypeMap, | ||
Optional: true, | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
}, | ||
|
||
"result": &schema.Schema{ | ||
Type: schema.TypeMap, | ||
Computed: true, | ||
Elem: &schema.Schema{ | ||
Type: schema.TypeString, | ||
}, | ||
}, | ||
}, | ||
} | ||
} | ||
|
||
func dataSourceRead(d *schema.ResourceData, meta interface{}) error { | ||
|
||
programI := d.Get("program").([]interface{}) | ||
query := d.Get("query").(map[string]interface{}) | ||
|
||
// This would be a ValidateFunc if helper/schema allowed these | ||
// to be applied to lists. | ||
if err := validateProgramAttr(programI); err != nil { | ||
return err | ||
} | ||
|
||
program := make([]string, len(programI)) | ||
for i, vI := range programI { | ||
program[i] = vI.(string) | ||
} | ||
|
||
cmd := exec.Command(program[0], program[1:]...) | ||
|
||
queryJson, err := json.Marshal(query) | ||
if err != nil { | ||
// Should never happen, since we know query will always be a map | ||
// from string to string, as guaranteed by d.Get and our schema. | ||
return err | ||
} | ||
|
||
cmd.Stdin = bytes.NewReader(queryJson) | ||
|
||
resultJson, err := cmd.Output() | ||
if err != nil { | ||
if exitErr, ok := err.(*exec.ExitError); ok { | ||
if exitErr.Stderr != nil && len(exitErr.Stderr) > 0 { | ||
return fmt.Errorf("failed to execute %q: %s", program[0], string(exitErr.Stderr)) | ||
} | ||
return fmt.Errorf("command %q failed with no error message", program[0]) | ||
} else { | ||
return fmt.Errorf("failed to execute %q: %s", program[0], err) | ||
} | ||
} | ||
|
||
result := map[string]string{} | ||
err = json.Unmarshal(resultJson, &result) | ||
if err != nil { | ||
return fmt.Errorf("command %q produced invalid JSON: %s", program[0], err) | ||
} | ||
|
||
d.Set("result", result) | ||
|
||
d.SetId("-") | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package external | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"os/exec" | ||
"path" | ||
"regexp" | ||
"testing" | ||
|
||
"github.com/hashicorp/terraform/helper/resource" | ||
"github.com/hashicorp/terraform/terraform" | ||
) | ||
|
||
const testDataSourceConfig_basic = ` | ||
data "external" "test" { | ||
program = ["%s", "cheese"] | ||
query = { | ||
value = "pizza" | ||
} | ||
} | ||
output "query_value" { | ||
value = "${data.external.test.result["query_value"]}" | ||
} | ||
output "argument" { | ||
value = "${data.external.test.result["argument"]}" | ||
} | ||
` | ||
|
||
func TestDataSource_basic(t *testing.T) { | ||
programPath, err := buildDataSourceTestProgram() | ||
if err != nil { | ||
t.Fatal(err) | ||
return | ||
} | ||
|
||
resource.UnitTest(t, resource.TestCase{ | ||
Providers: testProviders, | ||
Steps: []resource.TestStep{ | ||
resource.TestStep{ | ||
Config: fmt.Sprintf(testDataSourceConfig_basic, programPath), | ||
Check: func(s *terraform.State) error { | ||
_, ok := s.RootModule().Resources["data.external.test"] | ||
if !ok { | ||
return fmt.Errorf("missing data resource") | ||
} | ||
|
||
outputs := s.RootModule().Outputs | ||
|
||
if outputs["argument"] == nil { | ||
return fmt.Errorf("missing 'argument' output") | ||
} | ||
if outputs["query_value"] == nil { | ||
return fmt.Errorf("missing 'query_value' output") | ||
} | ||
|
||
if outputs["argument"].Value != "cheese" { | ||
return fmt.Errorf( | ||
"'argument' output is %q; want 'cheese'", | ||
outputs["argument"].Value, | ||
) | ||
} | ||
if outputs["query_value"].Value != "pizza" { | ||
return fmt.Errorf( | ||
"'query_value' output is %q; want 'pizza'", | ||
outputs["query_value"].Value, | ||
) | ||
} | ||
|
||
return nil | ||
}, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
const testDataSourceConfig_error = ` | ||
data "external" "test" { | ||
program = ["%s"] | ||
query = { | ||
fail = "true" | ||
} | ||
} | ||
` | ||
|
||
func TestDataSource_error(t *testing.T) { | ||
programPath, err := buildDataSourceTestProgram() | ||
if err != nil { | ||
t.Fatal(err) | ||
return | ||
} | ||
|
||
resource.UnitTest(t, resource.TestCase{ | ||
Providers: testProviders, | ||
Steps: []resource.TestStep{ | ||
resource.TestStep{ | ||
Config: fmt.Sprintf(testDataSourceConfig_error, programPath), | ||
ExpectError: regexp.MustCompile("I was asked to fail"), | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
func buildDataSourceTestProgram() (string, error) { | ||
// We have a simple Go program that we use as a stub for testing. | ||
cmd := exec.Command( | ||
"go", "install", | ||
"github.com/hashicorp/terraform/builtin/providers/external/test-programs/tf-acc-external-data-source", | ||
) | ||
err := cmd.Run() | ||
|
||
if err != nil { | ||
return "", fmt.Errorf("failed to build test stub program: %s", err) | ||
} | ||
|
||
programPath := path.Join( | ||
os.Getenv("GOPATH"), "bin", "tf-acc-external-data-source", | ||
) | ||
return programPath, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
50 changes: 50 additions & 0 deletions
50
builtin/providers/external/test-programs/tf-acc-external-data-source/main.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"fmt" | ||
"io/ioutil" | ||
"os" | ||
) | ||
|
||
// This is a minimal implementation of the external data source protocol | ||
// intended only for use in the provider acceptance tests. | ||
// | ||
// In practice it's likely not much harder to just write a real Terraform | ||
// plugin if you're going to be writing your data source in Go anyway; | ||
// this example is just in Go because we want to avoid introducing | ||
// additional language runtimes into the test environment. | ||
func main() { | ||
queryBytes, err := ioutil.ReadAll(os.Stdin) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
var query map[string]string | ||
err = json.Unmarshal(queryBytes, &query) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
if query["fail"] != "" { | ||
fmt.Fprintf(os.Stderr, "I was asked to fail\n") | ||
os.Exit(1) | ||
} | ||
|
||
var result = map[string]string{ | ||
"result": "yes", | ||
"query_value": query["value"], | ||
} | ||
|
||
if len(os.Args) >= 2 { | ||
result["argument"] = os.Args[1] | ||
} | ||
|
||
resultBytes, err := json.Marshal(result) | ||
if err != nil { | ||
panic(err) | ||
} | ||
|
||
os.Stdout.Write(resultBytes) | ||
os.Exit(0) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
package external | ||
|
||
import ( | ||
"fmt" | ||
"os/exec" | ||
) | ||
|
||
// validateProgramAttr is a validation function for the "program" attribute we | ||
// accept as input on our resources. | ||
// | ||
// The attribute is assumed to be specified in schema as a list of strings. | ||
func validateProgramAttr(v interface{}) error { | ||
args := v.([]interface{}) | ||
if len(args) < 1 { | ||
return fmt.Errorf("'program' list must contain at least one element") | ||
} | ||
|
||
for i, vI := range args { | ||
if _, ok := vI.(string); !ok { | ||
return fmt.Errorf( | ||
"'program' element %d is %T; a string is required", | ||
i, vI, | ||
) | ||
} | ||
} | ||
|
||
// first element is assumed to be an executable command, possibly found | ||
// using the PATH environment variable. | ||
_, err := exec.LookPath(args[0].(string)) | ||
if err != nil { | ||
return fmt.Errorf("can't find external program %q", args[0]) | ||
} | ||
|
||
return nil | ||
} |