diff --git a/.changes/unreleased/FEATURES-20241111-165312.yaml b/.changes/unreleased/FEATURES-20241111-165312.yaml
new file mode 100644
index 000000000..fe239a458
--- /dev/null
+++ b/.changes/unreleased/FEATURES-20241111-165312.yaml
@@ -0,0 +1,6 @@
+kind: FEATURES
+body: 'echoprovider: Introduced new `echoprovider` package, which contains a v6 Terraform
+ provider that can be used to test ephemeral resource data.'
+time: 2024-11-11T16:53:12.399802-05:00
+custom:
+ Issue: "389"
diff --git a/.changes/unreleased/NOTES-20241111-165206.yaml b/.changes/unreleased/NOTES-20241111-165206.yaml
new file mode 100644
index 000000000..37dc28142
--- /dev/null
+++ b/.changes/unreleased/NOTES-20241111-165206.yaml
@@ -0,0 +1,6 @@
+kind: NOTES
+body: 'echoprovider: The `echoprovider` package is considered experimental and may
+ be altered or removed in a subsequent release'
+time: 2024-11-11T16:52:06.287978-05:00
+custom:
+ Issue: "389"
diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml
index d0cde864e..05b8918d8 100644
--- a/.github/workflows/ci-go.yml
+++ b/.github/workflows/ci-go.yml
@@ -39,7 +39,7 @@ jobs:
terraform_version: ${{ matrix.terraform }}
terraform_wrapper: false
- run: go mod download
- - run: go test -coverprofile=coverage.out ./...
+ - run: go test -v -coverprofile=coverage.out ./...
env:
TF_ACC: "1"
- name: Remove wildcard suffix from TF version
diff --git a/echoprovider/doc.go b/echoprovider/doc.go
new file mode 100644
index 000000000..753097f82
--- /dev/null
+++ b/echoprovider/doc.go
@@ -0,0 +1,20 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+// Package echoprovider contains a protocol v6 Terraform provider that can be used to transfer data from
+// provider configuration to state via a managed resource. This is only meant for provider acceptance testing
+// of data that cannot be stored in Terraform artifacts (plan/state), such as an ephemeral resource.
+//
+// Example Usage:
+//
+// // Ephemeral resource that is under test
+// ephemeral "examplecloud_thing" "this" {
+// name = "thing-one"
+// }
+//
+// provider "echo" {
+// data = ephemeral.examplecloud_thing.this
+// }
+//
+// resource "echo" "test" {} // The `echo.test.data` attribute will contain the ephemeral data from `ephemeral.examplecloud_thing.this`
+package echoprovider
diff --git a/echoprovider/server.go b/echoprovider/server.go
new file mode 100644
index 000000000..dc4d90c8b
--- /dev/null
+++ b/echoprovider/server.go
@@ -0,0 +1,408 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package echoprovider
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/terraform-plugin-go/tfprotov6"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
+)
+
+// NewProviderServer returns the "echo" provider, which is a protocol v6 Terraform provider meant only to be used for testing
+// data which cannot be stored in Terraform artifacts (plan/state), such as an ephemeral resource. The "echo" provider can be included in
+// an acceptance test with the `(resource.TestCase).ProtoV6ProviderFactories` field, for example:
+//
+// resource.UnitTest(t, resource.TestCase{
+// // .. other TestCase fields
+// ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
+// "echo": echoprovider.NewProviderServer(),
+// },
+//
+// // .. TestSteps
+// })
+//
+// The "echo" provider configuration accepts in a dynamic "data" attribute, which will be stored in the "echo" managed resource "data" attribute, for example:
+//
+// // Ephemeral resource that is under test
+// ephemeral "examplecloud_thing" "this" {
+// name = "thing-one"
+// }
+//
+// provider "echo" {
+// data = ephemeral.examplecloud_thing.this
+// }
+//
+// resource "echo" "test" {} // The `echo.test.data` attribute will contain the ephemeral data from `ephemeral.examplecloud_thing.this`
+func NewProviderServer() func() (tfprotov6.ProviderServer, error) {
+ return func() (tfprotov6.ProviderServer, error) {
+ return &echoProviderServer{}, nil
+ }
+}
+
+// echoProviderServer is a lightweight protocol version 6 provider server that saves data from the provider configuration (which is considered ephemeral)
+// and then stores that data into state during ApplyResourceChange.
+//
+// As provider configuration is ephemeral, it's possible for the data to change between plan and apply. As a result of this, the echo provider
+// will never propose new changes after it has been created, making it immutable (during plan, echo will always use prior state for it's plan,
+// regardless of what the provider configuration is set to). This prevents the managed resource from continuously proposing new planned changes
+// if the ephemeral data changes.
+type echoProviderServer struct {
+ // The value of the "data" attribute during provider configuration. Will be directly echoed to the echo.data attribute.
+ providerConfigData tftypes.Value
+}
+
+const echoResourceType = "echo"
+
+func (e *echoProviderServer) providerSchema() *tfprotov6.Schema {
+ return &tfprotov6.Schema{
+ Block: &tfprotov6.SchemaBlock{
+ Description: "This provider is used to output the data attribute provided to the provider configuration into all resources instances of echo. " +
+ "This is only useful for testing ephemeral resources where the data isn't stored to state.",
+ DescriptionKind: tfprotov6.StringKindPlain,
+ Attributes: []*tfprotov6.SchemaAttribute{
+ {
+ Name: "data",
+ Type: tftypes.DynamicPseudoType,
+ Description: "Dynamic data to provide to the echo resource.",
+ DescriptionKind: tfprotov6.StringKindPlain,
+ Optional: true,
+ },
+ },
+ },
+ }
+}
+
+func (e *echoProviderServer) testResourceSchema() *tfprotov6.Schema {
+ return &tfprotov6.Schema{
+ Block: &tfprotov6.SchemaBlock{
+ Attributes: []*tfprotov6.SchemaAttribute{
+ {
+ Name: "data",
+ Type: tftypes.DynamicPseudoType,
+ Description: "Dynamic data that was provided to the provider configuration.",
+ DescriptionKind: tfprotov6.StringKindPlain,
+ Computed: true,
+ },
+ },
+ },
+ }
+}
+
+func (e *echoProviderServer) ApplyResourceChange(ctx context.Context, req *tfprotov6.ApplyResourceChangeRequest) (*tfprotov6.ApplyResourceChangeResponse, error) {
+ resp := &tfprotov6.ApplyResourceChangeResponse{}
+
+ if req.TypeName != echoResourceType {
+ resp.Diagnostics = []*tfprotov6.Diagnostic{
+ {
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unsupported Resource",
+ Detail: fmt.Sprintf("ApplyResourceChange was called for a resource type that is not supported by this provider: %q", req.TypeName),
+ },
+ }
+
+ return resp, nil
+ }
+
+ echoTestSchema := e.testResourceSchema()
+
+ plannedState, diag := dynamicValueToValue(echoTestSchema, req.PlannedState)
+ if diag != nil {
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+
+ return resp, nil
+ }
+
+ // Destroy Op, just return planned state, which is null
+ if plannedState.IsNull() {
+ resp.NewState = req.PlannedState
+ return resp, nil
+ }
+
+ // Take the provider config "data" attribute verbatim and put back into state. It shares the same type (DynamicPseudoType)
+ // as the echo "data" attribute.
+ newVal := tftypes.NewValue(echoTestSchema.ValueType(), map[string]tftypes.Value{
+ "data": e.providerConfigData,
+ })
+
+ newState, diag := valuetoDynamicValue(echoTestSchema, newVal)
+
+ if diag != nil {
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+
+ return resp, nil
+ }
+
+ resp.NewState = newState
+
+ return resp, nil
+}
+
+func (e *echoProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) {
+ return &tfprotov6.CallFunctionResponse{}, nil
+}
+
+func (e *echoProviderServer) ConfigureProvider(ctx context.Context, req *tfprotov6.ConfigureProviderRequest) (*tfprotov6.ConfigureProviderResponse, error) {
+ resp := &tfprotov6.ConfigureProviderResponse{}
+
+ configVal, diags := dynamicValueToValue(e.providerSchema(), req.Config)
+ if diags != nil {
+ resp.Diagnostics = append(resp.Diagnostics, diags)
+ return resp, nil
+ }
+
+ objVal := map[string]tftypes.Value{}
+ err := configVal.As(&objVal)
+ if err != nil {
+ diag := &tfprotov6.Diagnostic{
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Error reading Config",
+ Detail: err.Error(),
+ }
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+ return resp, nil //nolint:nilerr // error via diagnostic, not gRPC
+ }
+
+ dynamicDataVal, ok := objVal["data"]
+ if !ok {
+ diag := &tfprotov6.Diagnostic{
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: `Attribute "data" not found in config`,
+ }
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+ return resp, nil //nolint:nilerr // error via diagnostic, not gRPC
+ }
+
+ e.providerConfigData = dynamicDataVal.Copy()
+
+ return resp, nil
+}
+
+func (e *echoProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) {
+ return &tfprotov6.GetFunctionsResponse{}, nil
+}
+
+func (e *echoProviderServer) GetMetadata(ctx context.Context, req *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) {
+ return &tfprotov6.GetMetadataResponse{
+ Resources: []tfprotov6.ResourceMetadata{
+ {
+ TypeName: echoResourceType,
+ },
+ },
+ }, nil
+}
+
+func (e *echoProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.GetProviderSchemaRequest) (*tfprotov6.GetProviderSchemaResponse, error) {
+ return &tfprotov6.GetProviderSchemaResponse{
+ Provider: e.providerSchema(),
+ // MAINTAINER NOTE: This provider is only really built to support a single special resource type ("echo"). In the future, if we want
+ // to add more resource types to this provider, we'll likely need to refactor other RPCs in the provider server to handle that.
+ ResourceSchemas: map[string]*tfprotov6.Schema{
+ echoResourceType: e.testResourceSchema(),
+ },
+ }, nil
+}
+
+func (e *echoProviderServer) ImportResourceState(ctx context.Context, req *tfprotov6.ImportResourceStateRequest) (*tfprotov6.ImportResourceStateResponse, error) {
+ return &tfprotov6.ImportResourceStateResponse{
+ Diagnostics: []*tfprotov6.Diagnostic{
+ {
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unsupported Resource Operation",
+ Detail: "ImportResourceState is not supported by this provider.",
+ },
+ },
+ }, nil
+}
+
+func (e *echoProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) {
+ return &tfprotov6.MoveResourceStateResponse{
+ Diagnostics: []*tfprotov6.Diagnostic{
+ {
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unsupported Resource Operation",
+ Detail: "MoveResourceState is not supported by this provider.",
+ },
+ },
+ }, nil
+}
+
+func (e *echoProviderServer) PlanResourceChange(ctx context.Context, req *tfprotov6.PlanResourceChangeRequest) (*tfprotov6.PlanResourceChangeResponse, error) {
+ resp := &tfprotov6.PlanResourceChangeResponse{}
+
+ if req.TypeName != echoResourceType {
+ resp.Diagnostics = []*tfprotov6.Diagnostic{
+ {
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unsupported Resource",
+ Detail: fmt.Sprintf("PlanResourceChange was called for a resource type that is not supported by this provider: %q", req.TypeName),
+ },
+ }
+
+ return resp, nil
+ }
+
+ echoTestSchema := e.testResourceSchema()
+ priorState, diag := dynamicValueToValue(echoTestSchema, req.PriorState)
+ if diag != nil {
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+
+ return resp, nil
+ }
+
+ proposedNewState, diag := dynamicValueToValue(echoTestSchema, req.ProposedNewState)
+ if diag != nil {
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+
+ return resp, nil
+ }
+
+ // Destroying the resource, just return proposed new state (which is null)
+ if proposedNewState.IsNull() {
+ return &tfprotov6.PlanResourceChangeResponse{
+ PlannedState: req.ProposedNewState,
+ }, nil
+ }
+
+ // If the echo resource has prior state, don't plan anything new as it's valid for the ephemeral data to change
+ // between operations and we don't want to produce constant diffs. This resource is only for testing data, which a
+ // single plan/apply should suffice.
+ if !priorState.IsNull() {
+ return &tfprotov6.PlanResourceChangeResponse{
+ PlannedState: req.PriorState,
+ }, nil
+ }
+
+ // If we are creating, mark data as unknown in the plan.
+ //
+ // We can't set the proposed new state to the provider config data because it could change between plan/apply (provider config is ephemeral).
+ unknownVal := tftypes.NewValue(echoTestSchema.ValueType(), map[string]tftypes.Value{
+ "data": tftypes.NewValue(tftypes.DynamicPseudoType, tftypes.UnknownValue),
+ })
+
+ plannedState, diag := valuetoDynamicValue(echoTestSchema, unknownVal)
+ if diag != nil {
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+
+ return resp, nil
+ }
+
+ resp.PlannedState = plannedState
+
+ return resp, nil
+}
+
+func (e *echoProviderServer) ReadDataSource(ctx context.Context, req *tfprotov6.ReadDataSourceRequest) (*tfprotov6.ReadDataSourceResponse, error) {
+ return &tfprotov6.ReadDataSourceResponse{}, nil
+}
+
+func (e *echoProviderServer) ReadResource(ctx context.Context, req *tfprotov6.ReadResourceRequest) (*tfprotov6.ReadResourceResponse, error) {
+ // Just return current state, since the data doesn't need to be refreshed.
+ return &tfprotov6.ReadResourceResponse{
+ NewState: req.CurrentState,
+ }, nil
+}
+
+func (e *echoProviderServer) StopProvider(ctx context.Context, req *tfprotov6.StopProviderRequest) (*tfprotov6.StopProviderResponse, error) {
+ return &tfprotov6.StopProviderResponse{}, nil
+}
+
+func (e *echoProviderServer) UpgradeResourceState(ctx context.Context, req *tfprotov6.UpgradeResourceStateRequest) (*tfprotov6.UpgradeResourceStateResponse, error) {
+ resp := &tfprotov6.UpgradeResourceStateResponse{}
+
+ if req.TypeName != echoResourceType {
+ resp.Diagnostics = []*tfprotov6.Diagnostic{
+ {
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unsupported Resource",
+ Detail: fmt.Sprintf("UpgradeResourceState was called for a resource type that is not supported by this provider: %q", req.TypeName),
+ },
+ }
+
+ return resp, nil
+ }
+
+ // Define options to be used when unmarshalling raw state.
+ // IgnoreUndefinedAttributes will silently skip over fields in the JSON
+ // that do not have a matching entry in the schema.
+ unmarshalOpts := tfprotov6.UnmarshalOpts{
+ ValueFromJSONOpts: tftypes.ValueFromJSONOpts{
+ IgnoreUndefinedAttributes: true,
+ },
+ }
+
+ providerSchema := e.providerSchema()
+
+ if req.Version != providerSchema.Version {
+ resp.Diagnostics = []*tfprotov6.Diagnostic{
+ {
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unsupported Resource",
+ Detail: "UpgradeResourceState was called for echo, which does not support multiple schema versions",
+ },
+ }
+
+ return resp, nil
+ }
+
+ // Terraform CLI can call UpgradeResourceState even if the stored state
+ // version matches the current schema. Presumably this is to account for
+ // the previous terraform-plugin-sdk implementation, which handled some
+ // state fixups on behalf of Terraform CLI. This will attempt to roundtrip
+ // the prior RawState to a state matching the current schema.
+ rawStateValue, err := req.RawState.UnmarshalWithOpts(providerSchema.ValueType(), unmarshalOpts)
+
+ if err != nil {
+ diag := &tfprotov6.Diagnostic{
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unable to Read Previously Saved State for UpgradeResourceState",
+ Detail: "There was an error reading the saved resource state using the current resource schema: " + err.Error(),
+ }
+
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+
+ return resp, nil //nolint:nilerr // error via diagnostic, not gRPC
+ }
+
+ upgradedState, diag := valuetoDynamicValue(providerSchema, rawStateValue)
+
+ if diag != nil {
+ resp.Diagnostics = append(resp.Diagnostics, diag)
+
+ return resp, nil
+ }
+
+ resp.UpgradedState = upgradedState
+
+ return resp, nil
+}
+
+func (e *echoProviderServer) ValidateDataResourceConfig(ctx context.Context, req *tfprotov6.ValidateDataResourceConfigRequest) (*tfprotov6.ValidateDataResourceConfigResponse, error) {
+ return &tfprotov6.ValidateDataResourceConfigResponse{}, nil
+}
+
+func (e *echoProviderServer) ValidateProviderConfig(ctx context.Context, req *tfprotov6.ValidateProviderConfigRequest) (*tfprotov6.ValidateProviderConfigResponse, error) {
+ return &tfprotov6.ValidateProviderConfigResponse{}, nil
+}
+
+func (e *echoProviderServer) ValidateResourceConfig(ctx context.Context, req *tfprotov6.ValidateResourceConfigRequest) (*tfprotov6.ValidateResourceConfigResponse, error) {
+ return &tfprotov6.ValidateResourceConfigResponse{}, nil
+}
+
+func (e *echoProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) {
+ return &tfprotov6.OpenEphemeralResourceResponse{}, nil
+}
+
+func (e *echoProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) {
+ return &tfprotov6.RenewEphemeralResourceResponse{}, nil
+}
+
+func (e *echoProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) {
+ return &tfprotov6.CloseEphemeralResourceResponse{}, nil
+}
+
+func (e *echoProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) {
+ return &tfprotov6.ValidateEphemeralResourceConfigResponse{}, nil
+}
diff --git a/echoprovider/server_test.go b/echoprovider/server_test.go
new file mode 100644
index 000000000..bbc6dd656
--- /dev/null
+++ b/echoprovider/server_test.go
@@ -0,0 +1,283 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package echoprovider_test
+
+import (
+ "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/plancheck"
+ "github.com/hashicorp/terraform-plugin-testing/statecheck"
+ "github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
+ "github.com/hashicorp/terraform-plugin-testing/tfversion"
+)
+
+func TestEchoProviderServer_primitive(t *testing.T) {
+ t.Parallel()
+
+ resource.UnitTest(t, resource.TestCase{
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6
+ },
+ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
+ "echo": echoprovider.NewProviderServer(),
+ },
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ provider "echo" {
+ data = "hello world"
+ }
+ resource "echo" "test_one" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringExact("hello world")),
+ },
+ },
+ {
+ Config: `
+ provider "echo" {
+ data = 200
+ }
+ resource "echo" "test_two" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_two", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_two", tfjsonpath.New("data"), knownvalue.Int64Exact(200)),
+ },
+ },
+ {
+ Config: `
+ provider "echo" {
+ data = true
+ }
+ resource "echo" "test_three" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_three", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_three", tfjsonpath.New("data"), knownvalue.Bool(true)),
+ },
+ },
+ },
+ })
+}
+
+func TestEchoProviderServer_complex(t *testing.T) {
+ t.Parallel()
+
+ resource.UnitTest(t, resource.TestCase{
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6
+ },
+ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
+ "echo": echoprovider.NewProviderServer(),
+ },
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ provider "echo" {
+ data = tolist(["hello", "world"])
+ }
+ resource "echo" "test_one" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"),
+ knownvalue.ListExact([]knownvalue.Check{
+ knownvalue.StringExact("hello"),
+ knownvalue.StringExact("world"),
+ }),
+ ),
+ },
+ },
+ {
+ Config: `
+ provider "echo" {
+ data = tomap({"key1": "hello", "key2": "world"})
+ }
+ resource "echo" "test_two" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_two", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_two", tfjsonpath.New("data"),
+ knownvalue.MapExact(map[string]knownvalue.Check{
+ "key1": knownvalue.StringExact("hello"),
+ "key2": knownvalue.StringExact("world"),
+ }),
+ ),
+ },
+ },
+ {
+ Config: `
+ provider "echo" {
+ data = tomap({"key1": "hello", "key2": "world"})
+ }
+ resource "echo" "test_two" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ },
+ },
+ })
+}
+
+func TestEchoProviderServer_null(t *testing.T) {
+ t.Parallel()
+
+ resource.UnitTest(t, resource.TestCase{
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6
+ },
+ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
+ "echo": echoprovider.NewProviderServer(),
+ },
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ provider "echo" {}
+ resource "echo" "test_one" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.Null()),
+ },
+ },
+ },
+ })
+}
+
+func TestEchoProviderServer_unknown(t *testing.T) {
+ t.Parallel()
+
+ resource.UnitTest(t, resource.TestCase{
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6
+ },
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "random": {
+ Source: "hashicorp/random",
+ },
+ },
+ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
+ "echo": echoprovider.NewProviderServer(),
+ },
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ resource "random_string" "str" {
+ length = 12
+ }
+ provider "echo" {
+ data = random_string.str.result
+ }
+ resource "echo" "test_one" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringRegexp(regexp.MustCompile(`\S{12}`))),
+ },
+ },
+ {
+ Config: `
+ resource "random_string" "str" {
+ length = 12
+ }
+ provider "echo" {
+ data = random_string.str.result
+ }
+ resource "echo" "test_one" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ },
+ },
+ })
+}
+
+func TestEchoProviderServer_immutable(t *testing.T) {
+ t.Parallel()
+
+ resource.UnitTest(t, resource.TestCase{
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBelow(tfversion.Version1_0_0), // echo provider is protocol version 6
+ },
+ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
+ "echo": echoprovider.NewProviderServer(),
+ },
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ provider "echo" {
+ data = "original value"
+ }
+ resource "echo" "test_one" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectUnknownValue("echo.test_one", tfjsonpath.New("data")),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringExact("original value")),
+ },
+ },
+ {
+ // Despite the provider config data changing, the "echo.test_one" resource will never change as it's immutable.
+ Config: `
+ provider "echo" {
+ data = ["tuple", "of", "values"]
+ }
+ resource "echo" "test_one" {}
+ `,
+ ConfigPlanChecks: resource.ConfigPlanChecks{
+ PreApply: []plancheck.PlanCheck{
+ plancheck.ExpectEmptyPlan(),
+ },
+ },
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_one", tfjsonpath.New("data"), knownvalue.StringExact("original value")),
+ },
+ },
+ },
+ })
+}
diff --git a/echoprovider/tftypes.go b/echoprovider/tftypes.go
new file mode 100644
index 000000000..54e160a3c
--- /dev/null
+++ b/echoprovider/tftypes.go
@@ -0,0 +1,64 @@
+// Copyright (c) HashiCorp, Inc.
+// SPDX-License-Identifier: MPL-2.0
+
+package echoprovider
+
+import (
+ "github.com/hashicorp/terraform-plugin-go/tfprotov6"
+ "github.com/hashicorp/terraform-plugin-go/tftypes"
+)
+
+func valuetoDynamicValue(schema *tfprotov6.Schema, value tftypes.Value) (*tfprotov6.DynamicValue, *tfprotov6.Diagnostic) {
+ if schema == nil {
+ diag := &tfprotov6.Diagnostic{
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unable to Convert Value",
+ Detail: "Converting the Value to DynamicValue returned an unexpected error: missing schema",
+ }
+
+ return nil, diag
+ }
+
+ dynamicValue, err := tfprotov6.NewDynamicValue(schema.ValueType(), value)
+ if err != nil {
+ diag := &tfprotov6.Diagnostic{
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unable to Convert Value",
+ Detail: "Converting the Value to DynamicValue returned an unexpected error: " + err.Error(),
+ }
+
+ return &dynamicValue, diag
+ }
+
+ return &dynamicValue, nil
+}
+
+func dynamicValueToValue(schema *tfprotov6.Schema, dynamicValue *tfprotov6.DynamicValue) (tftypes.Value, *tfprotov6.Diagnostic) {
+ if schema == nil {
+ diag := &tfprotov6.Diagnostic{
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unable to Convert DynamicValue",
+ Detail: "Converting the DynamicValue to Value returned an unexpected error: missing schema",
+ }
+
+ return tftypes.NewValue(tftypes.Object{}, nil), diag
+ }
+
+ if dynamicValue == nil {
+ return tftypes.NewValue(schema.ValueType(), nil), nil
+ }
+
+ value, err := dynamicValue.Unmarshal(schema.ValueType())
+
+ if err != nil {
+ diag := &tfprotov6.Diagnostic{
+ Severity: tfprotov6.DiagnosticSeverityError,
+ Summary: "Unable to Convert DynamicValue",
+ Detail: "Converting the DynamicValue to Value returned an unexpected error: " + err.Error(),
+ }
+
+ return value, diag
+ }
+
+ return value, nil
+}
diff --git a/internal/testing/testsdk/providerserver/providerserver.go b/internal/testing/testsdk/providerserver/providerserver.go
index 4cc3f84ad..3c763914c 100644
--- a/internal/testing/testsdk/providerserver/providerserver.go
+++ b/internal/testing/testsdk/providerserver/providerserver.go
@@ -58,20 +58,16 @@ type ProviderServer struct {
Provider provider.Provider
}
-func (s ProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) {
- return &tfprotov6.CallFunctionResponse{}, nil
-}
-
-func (s ProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) {
- return &tfprotov6.GetFunctionsResponse{}, nil
-}
-
func (s ProviderServer) MoveResourceState(ctx context.Context, req *tfprotov6.MoveResourceStateRequest) (*tfprotov6.MoveResourceStateResponse, error) {
return &tfprotov6.MoveResourceStateResponse{}, nil
}
func (s ProviderServer) GetMetadata(ctx context.Context, request *tfprotov6.GetMetadataRequest) (*tfprotov6.GetMetadataResponse, error) {
resp := &tfprotov6.GetMetadataResponse{
+ // Functions and ephemeral resources not supported in this test SDK
+ Functions: []tfprotov6.FunctionMetadata{},
+ EphemeralResources: []tfprotov6.EphemeralResourceMetadata{},
+
ServerCapabilities: &tfprotov6.ServerCapabilities{
GetProviderSchemaOptional: true,
PlanDestroy: true,
@@ -252,6 +248,10 @@ func (s ProviderServer) GetProviderSchema(ctx context.Context, req *tfprotov6.Ge
s.Provider.Schema(ctx, providerReq, providerResp)
resp := &tfprotov6.GetProviderSchemaResponse{
+ // Functions and ephemeral resources not supported in this test SDK
+ Functions: map[string]*tfprotov6.Function{},
+ EphemeralResourceSchemas: map[string]*tfprotov6.Schema{},
+
DataSourceSchemas: map[string]*tfprotov6.Schema{},
Diagnostics: providerResp.Diagnostics,
Provider: providerResp.Schema,
@@ -795,3 +795,29 @@ func (s ProviderServer) ValidateResourceConfig(ctx context.Context, req *tfproto
return resp, nil
}
+
+// Functions are not currently implemented in this test SDK
+func (s ProviderServer) CallFunction(ctx context.Context, req *tfprotov6.CallFunctionRequest) (*tfprotov6.CallFunctionResponse, error) {
+ return &tfprotov6.CallFunctionResponse{}, nil
+}
+
+func (s ProviderServer) GetFunctions(ctx context.Context, req *tfprotov6.GetFunctionsRequest) (*tfprotov6.GetFunctionsResponse, error) {
+ return &tfprotov6.GetFunctionsResponse{}, nil
+}
+
+// Ephemeral resources are not currently implemented in this test SDK
+func (s ProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov6.OpenEphemeralResourceRequest) (*tfprotov6.OpenEphemeralResourceResponse, error) {
+ return &tfprotov6.OpenEphemeralResourceResponse{}, nil
+}
+
+func (s ProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov6.RenewEphemeralResourceRequest) (*tfprotov6.RenewEphemeralResourceResponse, error) {
+ return &tfprotov6.RenewEphemeralResourceResponse{}, nil
+}
+
+func (s ProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov6.CloseEphemeralResourceRequest) (*tfprotov6.CloseEphemeralResourceResponse, error) {
+ return &tfprotov6.CloseEphemeralResourceResponse{}, nil
+}
+
+func (s ProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov6.ValidateEphemeralResourceConfigRequest) (*tfprotov6.ValidateEphemeralResourceConfigResponse, error) {
+ return &tfprotov6.ValidateEphemeralResourceConfigResponse{}, nil
+}
diff --git a/internal/testing/testsdk/providerserver/providerserver_protov5.go b/internal/testing/testsdk/providerserver/providerserver_protov5.go
index 4e6452b67..5704b288a 100644
--- a/internal/testing/testsdk/providerserver/providerserver_protov5.go
+++ b/internal/testing/testsdk/providerserver/providerserver_protov5.go
@@ -69,10 +69,12 @@ func (s Protov5ProviderServer) GetProviderSchema(ctx context.Context, req *tfpro
s.Provider.Schema(ctx, providerReq, providerResp)
resp := &tfprotov5.GetProviderSchemaResponse{
- DataSourceSchemas: map[string]*tfprotov5.Schema{},
- Diagnostics: providerResp.Diagnostics,
- Provider: providerResp.Schema,
- ResourceSchemas: map[string]*tfprotov5.Schema{},
+ DataSourceSchemas: map[string]*tfprotov5.Schema{},
+ EphemeralResourceSchemas: map[string]*tfprotov5.Schema{},
+ Functions: map[string]*tfprotov5.Function{},
+ Diagnostics: providerResp.Diagnostics,
+ Provider: providerResp.Schema,
+ ResourceSchemas: map[string]*tfprotov5.Schema{},
ServerCapabilities: &tfprotov5.ServerCapabilities{
PlanDestroy: true,
},
@@ -116,3 +118,19 @@ func (s Protov5ProviderServer) ValidateDataSourceConfig(ctx context.Context, req
func (s Protov5ProviderServer) ValidateResourceTypeConfig(ctx context.Context, request *tfprotov5.ValidateResourceTypeConfigRequest) (*tfprotov5.ValidateResourceTypeConfigResponse, error) {
return &tfprotov5.ValidateResourceTypeConfigResponse{}, nil
}
+
+func (s Protov5ProviderServer) OpenEphemeralResource(ctx context.Context, req *tfprotov5.OpenEphemeralResourceRequest) (*tfprotov5.OpenEphemeralResourceResponse, error) {
+ return &tfprotov5.OpenEphemeralResourceResponse{}, nil
+}
+
+func (s Protov5ProviderServer) RenewEphemeralResource(ctx context.Context, req *tfprotov5.RenewEphemeralResourceRequest) (*tfprotov5.RenewEphemeralResourceResponse, error) {
+ return &tfprotov5.RenewEphemeralResourceResponse{}, nil
+}
+
+func (s Protov5ProviderServer) CloseEphemeralResource(ctx context.Context, req *tfprotov5.CloseEphemeralResourceRequest) (*tfprotov5.CloseEphemeralResourceResponse, error) {
+ return &tfprotov5.CloseEphemeralResourceResponse{}, nil
+}
+
+func (s Protov5ProviderServer) ValidateEphemeralResourceConfig(ctx context.Context, req *tfprotov5.ValidateEphemeralResourceConfigRequest) (*tfprotov5.ValidateEphemeralResourceConfigResponse, error) {
+ return &tfprotov5.ValidateEphemeralResourceConfigResponse{}, nil
+}
diff --git a/tfversion/versions.go b/tfversion/versions.go
index 4dcf5d14a..3db43e02e 100644
--- a/tfversion/versions.go
+++ b/tfversion/versions.go
@@ -30,10 +30,11 @@ var (
Version1_4_0 *version.Version = version.Must(version.NewVersion("1.4.0"))
// Version1_4_6 fixed inclusion of sensitive values in `terraform show -json` output.
// Reference: https://github.com/hashicorp/terraform/releases/tag/v1.4.6
- Version1_4_6 *version.Version = version.Must(version.NewVersion("1.4.6"))
- Version1_5_0 *version.Version = version.Must(version.NewVersion("1.5.0"))
- Version1_6_0 *version.Version = version.Must(version.NewVersion("1.6.0"))
- Version1_7_0 *version.Version = version.Must(version.NewVersion("1.7.0"))
- Version1_8_0 *version.Version = version.Must(version.NewVersion("1.8.0"))
- Version1_9_0 *version.Version = version.Must(version.NewVersion("1.9.0"))
+ Version1_4_6 *version.Version = version.Must(version.NewVersion("1.4.6"))
+ Version1_5_0 *version.Version = version.Must(version.NewVersion("1.5.0"))
+ Version1_6_0 *version.Version = version.Must(version.NewVersion("1.6.0"))
+ Version1_7_0 *version.Version = version.Must(version.NewVersion("1.7.0"))
+ Version1_8_0 *version.Version = version.Must(version.NewVersion("1.8.0"))
+ Version1_9_0 *version.Version = version.Must(version.NewVersion("1.9.0"))
+ Version1_10_0 *version.Version = version.Must(version.NewVersion("1.10.0"))
)
diff --git a/website/data/plugin-testing-nav-data.json b/website/data/plugin-testing-nav-data.json
index be9cff643..5eee96627 100644
--- a/website/data/plugin-testing-nav-data.json
+++ b/website/data/plugin-testing-nav-data.json
@@ -160,6 +160,10 @@
{
"title": "Terraform Configuration",
"path": "acceptance-tests/configuration"
+ },
+ {
+ "title": "Ephemeral Resources",
+ "path": "acceptance-tests/ephemeral-resources"
}
]
},
diff --git a/website/docs/plugin/testing/acceptance-tests/ephemeral-resources.mdx b/website/docs/plugin/testing/acceptance-tests/ephemeral-resources.mdx
new file mode 100644
index 000000000..36c6fea29
--- /dev/null
+++ b/website/docs/plugin/testing/acceptance-tests/ephemeral-resources.mdx
@@ -0,0 +1,271 @@
+---
+page_title: 'Plugin Development - Acceptance Testing: Ephemeral Resources'
+description: >-
+ Guidance on how to test ephemeral resources and data.
+---
+
+
+
+Ephemeral resource support is in technical preview and offered without compatibility promises until Terraform 1.10 is generally available.
+
+
+
+# Ephemeral Resources
+
+[Ephemeral Resources](/terraform/language/v1.10.x/resources/ephemeral) are an abstraction that allows Terraform to reference external data, similar to [data sources](/terraform/language/data-sources), without persisting that data to plan or state artifacts. The `terraform-plugin-testing` module exclusively uses Terraform plan and state artifacts for it's assertion-based test checks, like [plan checks](/terraform/plugin/testing/acceptance-tests/plan-checks) or [state checks](/terraform/plugin/testing/acceptance-tests/state-checks), which means that ephemeral resource data cannot be asserted using these methods alone.
+
+The following is a test for a hypothetical `examplecloud_secret` ephemeral resource which is referenced by a provider configuration that has a single managed resource. For this test to pass, the ephemeral `examplecloud_secret` resource must return valid data, specifically a kerberos `username`, `password`, and `realm`, which are used to configure the `dns` provider and create a DNS record via the `dns_a_record_set` managed resource.
+
+```go
+func TestExampleCloudSecret_DnsKerberos(t *testing.T) {
+ resource.UnitTest(t, resource.TestCase{
+ // Ephemeral resources are only available in 1.10 and later
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBelow(tfversion.Version1_10_0),
+ },
+ ExternalProviders: map[string]resource.ExternalProvider{
+ "dns": {
+ Source: "hashicorp/dns",
+ },
+ },
+ ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
+ "examplecloud": providerserver.NewProtocol5WithError(New()),
+ },
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ # Retrieves a secret containing user kerberos configuration
+ ephemeral "examplecloud_secret" "krb" {
+ name = "example_kerberos_user"
+ }
+
+ # Ephemeral data can be referenced in provider configuration
+ provider "dns" {
+ update {
+ server = "ns.example.com"
+ gssapi {
+ realm = ephemeral.examplecloud_secret.krb.secret_data.realm
+ username = ephemeral.examplecloud_secret.krb.secret_data.username
+ password = ephemeral.examplecloud_secret.krb.secret_data.password
+ }
+ }
+ }
+
+ # If we can create this DNS record successfully, then the ephemeral resource returned valid data.
+ resource "dns_a_record_set" "record_set" {
+ zone = "example.com."
+ addresses = [
+ "192.168.0.1",
+ "192.168.0.2",
+ "192.168.0.3",
+ ]
+ }
+ `,
+ },
+ },
+ })
+}
+```
+
+See the Terraform [ephemeral documentation](http://localhost:3000/terraform/language/v1.10.x/resources/ephemeral#referencing-ephemeral-resources) for more details on where ephemeral data can be referenced in configurations.
+
+## Testing ephemeral data with `echo` provider
+
+Test assertions on [result data](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-framework/ephemeral#OpenResponse.Result) returned by an ephemeral resource during [`Open`](/terraform/plugin/framework/ephemeral-resources/open) can be arranged using the `echoprovider` package.
+
+This package contains a [Protocol V6 Terraform Provider](/terraform/plugin/terraform-plugin-protocol#protocol-version-6) named `echo`, with a single managed resource also named `echo`. Using the `echo` provider configuration and an instance of the managed resource, ephemeral data can be "echoed" from the provider configuration into Terraform state, where it can be referenced in test assertions with [state checks](/terraform/plugin/testing/acceptance-tests/state-checks). For example:
+
+```terraform
+ephemeral "examplecloud_secret" "krb" {
+ name = "example_kerberos_user"
+}
+
+provider "echo" {
+ # Provide the ephemeral data we want to run test assertions against
+ data = ephemeral.examplecloud_secret.krb.secret_data
+}
+
+# The ephemeral data will be echoed into state
+resource "echo" "test_krb" {}
+```
+
+
+
+This provider is designed specifically to be used as a utility for acceptance testing ephemeral data and is only available via the `terraform-plugin-testing` Go module.
+
+
+
+### Using `echo` provider in acceptance tests
+
+First, we include the `echo` provider using the [`echoprovider.NewProviderServer`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/echoprovider#NewProviderServer) function in the `(TestCase).ProtoV6ProviderFactories` property:
+
+```go
+import (
+ // .. other imports
+
+ "github.com/hashicorp/terraform-plugin-testing/echoprovider"
+)
+
+func TestExampleCloudSecret(t *testing.T) {
+ resource.UnitTest(t, resource.TestCase{
+ // Ephemeral resources are only available in 1.10 and later
+ TerraformVersionChecks: []tfversion.TerraformVersionCheck{
+ tfversion.SkipBelow(tfversion.Version1_10_0),
+ },
+ // Include the provider we want to test: `examplecloud`
+ ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
+ "examplecloud": providerserver.NewProtocol5WithError(New()),
+ },
+ // Include `echo` as a v6 provider from `terraform-plugin-testing`
+ ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
+ "echo": echoprovider.NewProviderServer(),
+ },
+ Steps: []resource.TestStep{
+ // .. test step configurations can now use the `echo` and `examplecloud` providers
+ },
+ })
+}
+```
+
+After including both providers, our test step `Config` references the ephemeral data from `examplecloud_secret` in the `echo` provider configuration `data` attribute:
+
+```go
+func TestExampleCloudSecret(t *testing.T) {
+ resource.UnitTest(t, resource.TestCase{
+ // .. test case setup
+
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ ephemeral "examplecloud_secret" "krb" {
+ name = "example_kerberos_user"
+ }
+
+ provider "echo" {
+ data = ephemeral.examplecloud_secret.krb.secret_data
+ }
+
+ resource "echo" "test_krb" {}
+ `,
+ },
+ },
+ })
+}
+```
+
+The `echo.test_krb` managed resource has a single computed `data` attribute, which will contain the provider configuration `data` results. This data is then used in assertions with the [state check](/terraform/plugin/testing/acceptance-tests/state-checks) functionality:
+
+```go
+func TestExampleCloudSecret(t *testing.T) {
+ resource.UnitTest(t, resource.TestCase{
+ // .. test case setup
+
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ ephemeral "examplecloud_secret" "krb" {
+ name = "example_kerberos_user"
+ }
+
+ provider "echo" {
+ data = ephemeral.examplecloud_secret.krb.secret_data
+ }
+
+ resource "echo" "test_krb" {}
+ `,
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_krb", tfjsonpath.New("data").AtMapKey("realm"), knownvalue.StringExact("EXAMPLE.COM")),
+ statecheck.ExpectKnownValue("echo.test_krb", tfjsonpath.New("data").AtMapKey("username"), knownvalue.StringExact("john-doe")),
+ statecheck.ExpectKnownValue("echo.test_krb", tfjsonpath.New("data").AtMapKey("password"), knownvalue.StringRegexp(regexp.MustCompile(`^.{12}$`))),
+ },
+ },
+ },
+ })
+}
+```
+
+`data` is a `dynamic` attribute, so whatever [type](/terraform/language/expressions/types) you pass in will be directly reflected in the managed resource `data` attribute. In the config above, we reference an object (`secret_data`) from the ephemeral resource instance, so the resulting type of `echo.test_krb.data` is also an `object`.
+
+You can also reference the entire ephemeral resource instance for assertions, rather than specific attributes:
+
+```go
+func TestExampleCloudSecret(t *testing.T) {
+ resource.UnitTest(t, resource.TestCase{
+ // .. test case setup
+
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ ephemeral "examplecloud_secret" "krb" {
+ name = "example_kerberos_user"
+ }
+
+ provider "echo" {
+ data = ephemeral.examplecloud_secret.krb
+ }
+
+ resource "echo" "test_krb" {}
+ `,
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_krb", tfjsonpath.New("data").AtMapKey("name"), knownvalue.StringExact("example_kerberos_user")),
+ },
+ },
+ },
+ })
+}
+```
+
+### Caveats with `echo` provider
+
+Since data produced by an ephemeral resource is allowed to change between plan/apply operations, the `echo` resource has special handling to allow this data to be used in the `terraform-plugin-testing` Go module without producing confusing error messages:
+
+* During plan, if the `echo` resource is being created, the `data` attribute will always be marked as unknown.
+* During plan, if the `echo` resource already exists and is not being destroyed, prior state will always be fully preserved regardless of changes to the provider configuration. This essentially means an instance of the `echo` resource is immutable.
+* During refresh, the prior state of the `echo` resource is always returned, regardless of changes to the provider configuration.
+
+Due to this special handling, if multiple test steps are required for testing data, provider developers should create new instances of `echo` for each new test step, for example:
+
+```go
+func TestExampleCloudSecret(t *testing.T) {
+ resource.UnitTest(t, resource.TestCase{
+ // .. test case setup
+
+ Steps: []resource.TestStep{
+ {
+ Config: `
+ ephemeral "examplecloud_secret" "krb" {
+ name = "user_one"
+ }
+
+ provider "echo" {
+ data = ephemeral.examplecloud_secret.krb
+ }
+
+ # First test object -> 1
+ resource "echo" "test_krb_one" {}
+ `,
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_krb_one", tfjsonpath.New("data").AtMapKey("name"), knownvalue.StringExact("user_one")),
+ },
+ },
+ {
+ Config: `
+ ephemeral "examplecloud_secret" "krb" {
+ name = "user_two"
+ }
+
+ provider "echo" {
+ data = ephemeral.examplecloud_secret.krb
+ }
+
+ # New test object -> 2
+ resource "echo" "test_krb_two" {}
+ `,
+ ConfigStateChecks: []statecheck.StateCheck{
+ statecheck.ExpectKnownValue("echo.test_krb_two", tfjsonpath.New("data").AtMapKey("name"), knownvalue.StringExact("user_two")),
+ },
+ },
+ },
+ })
+}
+```
\ No newline at end of file