diff --git a/internal/provider/data_source_test.go b/internal/provider/data_source_test.go index 0454cfa3..ed838327 100644 --- a/internal/provider/data_source_test.go +++ b/internal/provider/data_source_test.go @@ -116,6 +116,75 @@ func TestDataSource_error(t *testing.T) { }) } +const testDataSourceConfig_workingDir = ` +data "external" "test" { + program = ["%s"] + working_dir = "%s" + + query = { + value = "test" + } +} + +output "working_dir" { + value = "${data.external.test.result["working_dir"]}" +} + +output "result" { + value = "${data.external.test.result["result"]}" +} +` + +func TestDataSource_workingDirectory(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + workingDir := "/tmp" + + resource.UnitTest(t, resource.TestCase{ + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testDataSourceConfig_workingDir, programPath, workingDir), + 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["working_dir"] == nil { + return fmt.Errorf("missing 'working_dir' output") + } + if outputs["result"] == nil { + return fmt.Errorf("missing 'result' output") + } + + if outputs["working_dir"].Value != workingDir { + return fmt.Errorf( + "'working_dir' output is %q; want %q", + outputs["working_dir"].Value, + workingDir, + ) + } + if outputs["result"].Value != "yes" { + return fmt.Errorf( + "'result' output is %q; want 'yes'", + outputs["result"].Value, + ) + } + + return nil + }, + }, + }, + }) +} + // Reference: https://github.com/hashicorp/terraform-provider-external/issues/110 func TestDataSource_Program_OnlyEmptyString(t *testing.T) { resource.UnitTest(t, resource.TestCase{ diff --git a/internal/provider/ephemeral.go b/internal/provider/ephemeral.go new file mode 100644 index 00000000..8c90ade4 --- /dev/null +++ b/internal/provider/ephemeral.go @@ -0,0 +1,283 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "os/exec" + "runtime" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" + "github.com/hashicorp/terraform-plugin-framework/ephemeral/schema" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +var ( + _ ephemeral.EphemeralResource = (*externalEphemeralResource)(nil) +) + +func NewExternalEphemeralResource() ephemeral.EphemeralResource { + return &externalEphemeralResource{} +} + +type externalEphemeralResource struct{} + +func (e *externalEphemeralResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) { + resp.TypeName = req.ProviderTypeName +} + +func (e *externalEphemeralResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) { + resp.Schema = schema.Schema{ + Description: "The `external` ephemeral resource allows an external program implementing a specific protocol " + + "(defined below) to act as an ephemeral resource, exposing arbitrary data for use elsewhere in the Terraform " + + "configuration without storing the data in state.\n" + + "\n" + + "**Warning** This mechanism is provided as an \"escape hatch\" for exceptional situations where a " + + "first-class Terraform provider is not more appropriate. Its capabilities are limited in comparison " + + "to a true ephemeral resource, and implementing an ephemeral resource via an external program is likely to hurt the " + + "portability of your Terraform configuration by creating dependencies on external programs and " + + "libraries that may not be available (or may need to be used differently) on different operating " + + "systems.\n" + + "\n" + + "**Warning** Terraform Enterprise does not guarantee availability of any particular language runtimes " + + "or external programs beyond standard shell utilities, so it is not recommended to use this ephemeral resource " + + "within configurations that are applied within Terraform Enterprise.", + + Attributes: map[string]schema.Attribute{ + "program": schema.ListAttribute{ + Description: "A list of strings, whose first element is the program to run and whose " + + "subsequent elements are optional command line arguments to the program. Terraform does " + + "not execute the program through a shell, so it is not necessary to escape shell " + + "metacharacters nor add quotes around arguments containing spaces.", + ElementType: types.StringType, + Required: true, + Validators: []validator.List{ + listvalidator.SizeAtLeast(1), + }, + }, + + "working_dir": schema.StringAttribute{ + Description: "Working directory of the program. If not supplied, the program will run " + + "in the current directory.", + Optional: true, + }, + + "query": schema.MapAttribute{ + Description: "A map of string values to pass to the external program as the query " + + "arguments. If not supplied, the program will receive an empty object as its input.", + ElementType: types.StringType, + Optional: true, + }, + + "result": schema.MapAttribute{ + Description: "A map of string values returned from the external program.", + ElementType: types.StringType, + Computed: true, + }, + }, + } +} + +func (e *externalEphemeralResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) { + var config externalEphemeralResourceModelV0 + + diags := req.Config.Get(ctx, &config) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + var program []types.String + + diags = config.Program.ElementsAs(ctx, &program, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + filteredProgram := make([]string, 0, len(program)) + + for _, programArgRaw := range program { + if programArgRaw.IsNull() || programArgRaw.ValueString() == "" { + continue + } + + filteredProgram = append(filteredProgram, programArgRaw.ValueString()) + } + + if len(filteredProgram) == 0 { + resp.Diagnostics.AddAttributeError( + path.Root("program"), + "External Program Missing", + "The ephemeral resource was configured without a program to execute. Verify the configuration contains at least one non-empty value.", + ) + return + } + + var query map[string]types.String + + diags = config.Query.ElementsAs(ctx, &query, false) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + filteredQuery := make(map[string]string) + for key, value := range query { + // Preserve v2.2.3 and earlier behavior of filtering whole map elements + // with null values. + // Reference: https://github.com/hashicorp/terraform-provider-external/issues/208 + // + // The external program protocol could be updated to support null values + // as a breaking change by marshaling map[string]*string to JSON. + // Reference: https://github.com/hashicorp/terraform-provider-external/issues/209 + if value.IsNull() { + continue + } + + filteredQuery[key] = value.ValueString() + } + + queryJson, err := json.Marshal(filteredQuery) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("query"), + "Query Handling Failed", + "The ephemeral resource 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), + ) + return + } + + // first element is assumed to be an executable command, possibly found + // using the PATH environment variable. + _, err = exec.LookPath(filteredProgram[0]) + + // This is a workaround to preserve pre-existing behaviour prior to the upgrade to Go 1.19. + // Reference: https://github.com/hashicorp/terraform-provider-external/pull/192 + // + // This workaround will be removed once a warning is being issued to notify practitioners + // of a change in behaviour. + // Reference: https://github.com/hashicorp/terraform-provider-external/issues/197 + if errors.Is(err, exec.ErrDot) { + err = nil + } + + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("program"), + "External Program Lookup Failed", + "The ephemeral resource received an unexpected error while attempting to parse the query. "+ + `The ephemeral resource 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), + ) + return + } + + workingDir := config.WorkingDir.ValueString() + + cmd := exec.CommandContext(ctx, filteredProgram[0], filteredProgram[1:]...) + + // This is a workaround to preserve pre-existing behaviour prior to the upgrade to Go 1.19. + // Reference: https://github.com/hashicorp/terraform-provider-external/pull/192 + // + // This workaround will be removed once a warning is being issued to notify practitioners + // of a change in behaviour. + // Reference: https://github.com/hashicorp/terraform-provider-external/issues/197 + if errors.Is(cmd.Err, exec.ErrDot) { + cmd.Err = nil + } + + cmd.Dir = workingDir + cmd.Stdin = bytes.NewReader(queryJson) + + var stderr strings.Builder + cmd.Stderr = &stderr + + tflog.Trace(ctx, "Executing external program", map[string]interface{}{"program": cmd.String()}) + + resultJson, err := cmd.Output() + + stderrStr := stderr.String() + + tflog.Trace(ctx, "Executed external program", map[string]interface{}{"program": cmd.String(), "output": string(resultJson), "stderr": stderrStr}) + + if err != nil { + if len(stderrStr) > 0 { + resp.Diagnostics.AddAttributeError( + path.Root("program"), + "External Program Execution Failed", + "The ephemeral resource received an unexpected error while attempting to execute the program."+ + fmt.Sprintf("\n\nProgram: %s", cmd.Path)+ + fmt.Sprintf("\nError Message: %s", stderrStr)+ + fmt.Sprintf("\nState: %s", err), + ) + return + } + + resp.Diagnostics.AddAttributeError( + path.Root("program"), + "External Program Execution Failed", + "The ephemeral resource 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), + ) + return + } + + result := map[string]string{} + err = json.Unmarshal(resultJson, &result) + if err != nil { + resp.Diagnostics.AddAttributeError( + path.Root("program"), + "Unexpected External Program Results", + `The ephemeral resource 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), + ) + return + } + + config.Result, diags = types.MapValueFrom(ctx, types.StringType, result) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + diags = resp.Result.Set(ctx, config) + resp.Diagnostics.Append(diags...) +} + +type externalEphemeralResourceModelV0 struct { + Program types.List `tfsdk:"program"` + WorkingDir types.String `tfsdk:"working_dir"` + Query types.Map `tfsdk:"query"` + Result types.Map `tfsdk:"result"` +} diff --git a/internal/provider/ephemeral_test.go b/internal/provider/ephemeral_test.go new file mode 100644 index 00000000..d6353547 --- /dev/null +++ b/internal/provider/ephemeral_test.go @@ -0,0 +1,437 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package provider + +import ( + "fmt" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/echoprovider" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/knownvalue" + "github.com/hashicorp/terraform-plugin-testing/statecheck" + "github.com/hashicorp/terraform-plugin-testing/tfjsonpath" + "github.com/hashicorp/terraform-plugin-testing/tfversion" +) + +const testEphemeralExternalConfig_basic = ` +ephemeral "external" "test" { + program = ["%s", "cheese"] + + query = { + value = "pizza" + } +} + +provider "echo" { + data = ephemeral.external.test.result +} + +resource "echo" "test" {} +` + +func TestEphemeralExternal_basic(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testEphemeralExternalConfig_basic, programPath), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("result"), + knownvalue.StringExact("yes"), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("query_value"), + knownvalue.StringExact("pizza"), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("argument"), + knownvalue.StringExact("cheese"), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("value"), + knownvalue.StringExact("pizza"), + ), + }, + }, + }, + }) +} + +const testEphemeralExternalConfig_error = ` +ephemeral "external" "test" { + program = ["%s"] + + query = { + fail = "true" + } +} +` + +func TestEphemeralExternal_error(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testEphemeralExternalConfig_error, programPath), + ExpectError: regexp.MustCompile("I was asked to fail"), + }, + }, + }) +} + +const testEphemeralExternalConfig_workingDir = ` +ephemeral "external" "test" { + program = ["%s"] + working_dir = "%s" + + query = { + value = "test" + } +} + +provider "echo" { + data = ephemeral.external.test.result +} + +resource "echo" "test" {} +` + +func TestEphemeralExternal_workingDirectory(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + workingDir := "/tmp" + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testEphemeralExternalConfig_workingDir, programPath, workingDir), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("working_dir"), + knownvalue.StringExact(workingDir), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("result"), + knownvalue.StringExact("yes"), + ), + }, + }, + }, + }) +} + +const testEphemeralExternalConfig_emptyQuery = ` +ephemeral "external" "test" { + program = ["%s"] + + query = {} +} + +provider "echo" { + data = ephemeral.external.test.result +} + +resource "echo" "test" {} +` + +func TestEphemeralExternal_emptyQuery(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(testEphemeralExternalConfig_emptyQuery, programPath), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("result"), + knownvalue.StringExact("yes"), + ), + }, + }, + }, + }) +} + +const testEphemeralExternalConfig_missingProgram = ` +ephemeral "external" "test" { + program = [] +} +` + +func TestEphemeralExternal_missingProgram(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: testEphemeralExternalConfig_missingProgram, + ExpectError: regexp.MustCompile("Invalid Attribute Value"), + }, + }, + }) +} + +func TestEphemeralExternal_Program_OnlyEmptyString(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: ` + ephemeral "external" "test" { + program = [ + "", # e.g. a variable that became empty + ] + + query = { + value = "valuetest" + } + } + `, + ExpectError: regexp.MustCompile(`External Program Missing`), + }, + }, + }) +} + +func TestEphemeralExternal_Program_PathAndEmptyString(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + ephemeral "external" "test" { + program = [ + %[1]q, + "", # e.g. a variable that became empty + ] + + query = { + value = "valuetest" + } + } + + provider "echo" { + data = ephemeral.external.test.result + } + + resource "echo" "test" {} + `, programPath), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("result"), + knownvalue.StringExact("yes"), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("query_value"), + knownvalue.StringExact("valuetest"), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("value"), + knownvalue.StringExact("valuetest"), + ), + }, + }, + }, + }) +} + +func TestEphemeralExternal_Program_EmptyStringAndNullValues(t *testing.T) { + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: ` + ephemeral "external" "test" { + program = [ + null, "", # e.g. a variable that became empty + ] + + query = { + value = "valuetest" + } + } + `, + ExpectError: regexp.MustCompile(`External Program Missing`), + }, + }, + }) +} + +func TestEphemeralExternal_Query_EmptyElementValue(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + ephemeral "external" "test" { + program = [%[1]q] + + query = { + value = "" + } + } + + provider "echo" { + data = ephemeral.external.test.result + } + + resource "echo" "test" {} + `, programPath), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("result"), + knownvalue.StringExact("yes"), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("query_value"), + knownvalue.StringExact(""), + ), + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("value"), + knownvalue.StringExact(""), + ), + }, + }, + }, + }) +} + +func TestEphemeralExternal_Query_NullElementValue(t *testing.T) { + programPath, err := buildDataSourceTestProgram() + if err != nil { + t.Fatal(err) + return + } + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_10_0), + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "echo": echoprovider.NewProviderServer(), + }, + ProtoV5ProviderFactories: protoV5ProviderFactories(), + Steps: []resource.TestStep{ + { + Config: fmt.Sprintf(` + ephemeral "external" "test" { + program = [%[1]q] + + query = { + # Program will return exit status 1 if the "fail" key is present. + fail = null + } + } + + provider "echo" { + data = ephemeral.external.test.result + } + + resource "echo" "test" {} + `, programPath), + ConfigStateChecks: []statecheck.StateCheck{ + statecheck.ExpectKnownValue( + "echo.test", + tfjsonpath.New("data").AtMapKey("result"), + knownvalue.StringExact("yes"), + ), + // The test passes by successfully running without the "fail" key + // causing the external program to exit with status 1 + }, + }, + }, + }) +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 4278e9f9..5b438981 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -7,11 +7,12 @@ import ( "context" "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/ephemeral" "github.com/hashicorp/terraform-plugin-framework/provider" "github.com/hashicorp/terraform-plugin-framework/resource" ) -var _ provider.Provider = (*externalProvider)(nil) +var _ provider.ProviderWithEphemeralResources = (*externalProvider)(nil) func New() provider.Provider { return &externalProvider{} @@ -37,5 +38,11 @@ func (p *externalProvider) Resources(ctx context.Context) []func() resource.Reso return nil } +func (p *externalProvider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource { + return []func() ephemeral.EphemeralResource{ + NewExternalEphemeralResource, + } +} + func (p *externalProvider) Schema(context.Context, provider.SchemaRequest, *provider.SchemaResponse) { } diff --git a/internal/provider/test-programs/tf-acc-external-data-source/main.go b/internal/provider/test-programs/tf-acc-external-data-source/main.go index e11df018..8063a982 100644 --- a/internal/provider/test-programs/tf-acc-external-data-source/main.go +++ b/internal/provider/test-programs/tf-acc-external-data-source/main.go @@ -46,6 +46,11 @@ func main() { result["argument"] = os.Args[1] } + // Add working directory to result + if wd, err := os.Getwd(); err == nil { + result["working_dir"] = wd + } + for queryKey, queryValue := range query { if queryValue != nil { result[queryKey] = *queryValue