diff --git a/go.mod b/go.mod index 780dce85..68bf0aeb 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module github.com/terraform-providers/terraform-provider-external go 1.15 -require github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 +require ( + github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1 +) diff --git a/internal/provider/data_source.go b/internal/provider/data_source.go index 5d8cef9c..97fbbcdb 100644 --- a/internal/provider/data_source.go +++ b/internal/provider/data_source.go @@ -4,9 +4,12 @@ import ( "bytes" "context" "encoding/json" + "fmt" "log" "os/exec" + "runtime" + "github.com/hashicorp/go-cty/cty" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -41,6 +44,7 @@ func dataSource() *schema.Resource { Elem: &schema.Schema{ Type: schema.TypeString, }, + MinItems: 1, }, "working_dir": { @@ -79,28 +83,54 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{ workingDir := d.Get("working_dir").(string) 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 diag.FromErr(err) - } - program := make([]string, len(programI)) for i, vI := range programI { program[i] = vI.(string) } - cmd := exec.CommandContext(ctx, program[0], program[1:]...) + queryJson, err := json.Marshal(query) + if err != nil { + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "Query Handling Failed", + Detail: "The data source received an unexpected error while attempting to parse the query. " + + "This is always a bug in the external provider code and should be reported to the provider developers." + + fmt.Sprintf("\n\nError: %s", err), + AttributePath: cty.GetAttrPath("query"), + }, + } + } - cmd.Dir = workingDir + // first element is assumed to be an executable command, possibly found + // using the PATH environment variable. + _, err = exec.LookPath(program[0]) - 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 diag.FromErr(err) + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "External Program Lookup Failed", + Detail: `The data source received an unexpected error while attempting to find the program. + +The program must be accessible according to the platform where Terraform is running. + +If the expected program should be automatically found on the platform where Terraform is running, ensure that the program is in an expected directory. On Unix-based platforms, these directories are typically searched based on the '$PATH' environment variable. On Windows-based platforms, these directories are typically searched based on the '%PATH%' environment variable. + +If the expected program is relative to the Terraform configuration, it is recommended that the program name includes the interpolated value of 'path.module' before the program name to ensure that it is compatible with varying module usage. For example: "${path.module}/my-program" + +The program must also be executable according to the platform where Terraform is running. On Unix-based platforms, the file on the filesystem must have the executable bit set. On Windows-based platforms, no action is typically necessary. +` + + fmt.Sprintf("\nPlatform: %s", runtime.GOOS) + + fmt.Sprintf("\nProgram: %s", program[0]) + + fmt.Sprintf("\nError: %s", err), + AttributePath: cty.GetAttrPath("program"), + }, + } } + cmd := exec.CommandContext(ctx, program[0], program[1:]...) + cmd.Dir = workingDir cmd.Stdin = bytes.NewReader(queryJson) resultJson, err := cmd.Output() @@ -108,18 +138,62 @@ func dataSourceRead(ctx context.Context, d *schema.ResourceData, meta interface{ if err != nil { if exitErr, ok := err.(*exec.ExitError); ok { if exitErr.Stderr != nil && len(exitErr.Stderr) > 0 { - return diag.Errorf("failed to execute %q: %s", program[0], string(exitErr.Stderr)) + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "External Program Execution Failed", + Detail: "The data source received an unexpected error while attempting to execute the program." + + fmt.Sprintf("\n\nProgram: %s", cmd.Path) + + fmt.Sprintf("\nError Message: %s", string(exitErr.Stderr)) + + fmt.Sprintf("\nState: %s", err), + AttributePath: cty.GetAttrPath("program"), + }, + } } - return diag.Errorf("command %q failed with no error message", program[0]) - } else { - return diag.Errorf("failed to execute %q: %s", program[0], err) + + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "External Program Execution Failed", + Detail: "The data source received an unexpected error while attempting to execute the program.\n\n" + + "The program was executed, however it returned no additional error messaging." + + fmt.Sprintf("\n\nProgram: %s", cmd.Path) + + fmt.Sprintf("\nState: %s", err), + AttributePath: cty.GetAttrPath("program"), + }, + } + } + + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "External Program Execution Failed", + Detail: "The data source received an unexpected error while attempting to execute the program." + + fmt.Sprintf("\n\nProgram: %s", cmd.Path) + + fmt.Sprintf("\nError: %s", err), + AttributePath: cty.GetAttrPath("program"), + }, } } result := map[string]string{} err = json.Unmarshal(resultJson, &result) if err != nil { - return diag.Errorf("command %q produced invalid JSON: %s", program[0], err) + return diag.Diagnostics{ + { + Severity: diag.Error, + Summary: "Unexpected External Program Results", + Detail: `The data source received unexpected results after executing the program. + +Program output must be a JSON encoded map of string keys and string values. + +If the error is unclear, the output can be viewed by enabling Terraform's logging at TRACE level. Terraform documentation on logging: https://www.terraform.io/internals/debugging +` + + fmt.Sprintf("\nProgram: %s", cmd.Path) + + fmt.Sprintf("\nResult Error: %s", err), + AttributePath: cty.GetAttrPath("program"), + }, + } } d.Set("result", result) diff --git a/internal/provider/util.go b/internal/provider/util.go deleted file mode 100644 index c6d1b648..00000000 --- a/internal/provider/util.go +++ /dev/null @@ -1,35 +0,0 @@ -package provider - -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 -}