Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

all: Add ephemeral resource schema and lifecycle tests #283

Merged
merged 25 commits into from
Nov 19, 2024
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
44d5ec0
echo resource
austinvalle Nov 1, 2024
76f20a3
ephemeral resource with tests
austinvalle Nov 1, 2024
9228f0e
move echo provider to plugin testing
austinvalle Nov 5, 2024
c58577d
update with testing echo provider
austinvalle Nov 5, 2024
fa306eb
switch to constants
austinvalle Nov 6, 2024
e2642c4
constants
austinvalle Nov 6, 2024
5b86f85
rename files
austinvalle Nov 6, 2024
73ee64c
added basic ephemeral schema test
austinvalle Nov 6, 2024
17a5b5e
add protov6 ephemeral resource test
austinvalle Nov 6, 2024
a1ec18a
receiver variable
austinvalle Nov 7, 2024
28f8335
add lifecycle tests
austinvalle Nov 8, 2024
25a477d
add nested attributes + dynamics to ephemeral schema test
austinvalle Nov 8, 2024
0ebac79
update interface checks
austinvalle Nov 8, 2024
ad784d3
Merge branch 'main' into av/ephemeral-resources
austinvalle Nov 8, 2024
1a30d46
add copyright headers
austinvalle Nov 8, 2024
dc03785
update resource type name
austinvalle Nov 8, 2024
cd4267f
use alpha skip for deferred action tests
austinvalle Nov 8, 2024
3b1e5c9
update plugin testing dep
austinvalle Nov 11, 2024
a504cf7
update to latest testing commit with new server behavior
austinvalle Nov 12, 2024
926ba3a
switch to using depends_on
austinvalle Nov 13, 2024
42f1a45
share the data path from a variable
austinvalle Nov 14, 2024
442a51a
Merge branch 'main' into av/ephemeral-resources
austinvalle Nov 15, 2024
046f43c
adjust the newly fixed RC1 tests + adjust test assertion order
austinvalle Nov 15, 2024
91b2037
Merge branch 'av/ephemeral-resources' of github.com:hashicorp/terrafo…
austinvalle Nov 15, 2024
bcc9749
Merge branch 'main' into av/ephemeral-resources
austinvalle Nov 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ require (
github.com/hashicorp/terraform-plugin-go v0.25.0
github.com/hashicorp/terraform-plugin-mux v0.17.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0
github.com/hashicorp/terraform-plugin-testing v1.10.0
github.com/hashicorp/terraform-plugin-testing v1.10.1-0.20241112232341-e5b632d36863
github.com/zclconf/go-cty v1.15.0
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ github.com/hashicorp/terraform-plugin-mux v0.17.0 h1:/J3vv3Ps2ISkbLPiZOLspFcIZ0v
github.com/hashicorp/terraform-plugin-mux v0.17.0/go.mod h1:yWuM9U1Jg8DryNfvCp+lH70WcYv6D8aooQxxxIzFDsE=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0 h1:wyKCCtn6pBBL46c1uIIBNUOWlNfYXfXpVo16iDyLp8Y=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.35.0/go.mod h1:B0Al8NyYVr8Mp/KLwssKXG1RqnTk7FySqSn4fRuLNgw=
github.com/hashicorp/terraform-plugin-testing v1.10.0 h1:2+tmRNhvnfE4Bs8rB6v58S/VpqzGC6RCh9Y8ujdn+aw=
github.com/hashicorp/terraform-plugin-testing v1.10.0/go.mod h1:iWRW3+loP33WMch2P/TEyCxxct/ZEcCGMquSLSCVsrc=
github.com/hashicorp/terraform-plugin-testing v1.10.1-0.20241112232341-e5b632d36863 h1:1BV2qGnJyCG0j9IuC8tETeEqGm5n32oqZWnPpMCPYJI=
github.com/hashicorp/terraform-plugin-testing v1.10.1-0.20241112232341-e5b632d36863/go.mod h1:KO4JzpCD5O7tSKZk20W70X2tpna9pIEoiTQD6cS8VrU=
github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI=
github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM=
github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ=
Expand Down
10 changes: 5 additions & 5 deletions internal/framework5provider/deferred_action_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ func TestDeferredActionResource_ProviderDeferral(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_9_0),
tfversion.SkipIfNotPrerelease(),
tfversion.SkipIfNotAlpha(),
},
AdditionalCLIOptions: &resource.AdditionalCLIOptions{
Plan: resource.PlanOptions{AllowDeferral: true},
Expand Down Expand Up @@ -90,7 +90,7 @@ func TestDeferredActionPlanModificationResource_ProviderDeferral(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_9_0),
tfversion.SkipIfNotPrerelease(),
tfversion.SkipIfNotAlpha(),
},
AdditionalCLIOptions: &resource.AdditionalCLIOptions{
Plan: resource.PlanOptions{AllowDeferral: true},
Expand Down Expand Up @@ -169,7 +169,7 @@ func TestDeferredActionResource_ModifyPlanDeferral(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_9_0),
tfversion.SkipIfNotPrerelease(),
tfversion.SkipIfNotAlpha(),
},
AdditionalCLIOptions: &resource.AdditionalCLIOptions{
Plan: resource.PlanOptions{AllowDeferral: true},
Expand Down Expand Up @@ -211,7 +211,7 @@ func TestDeferredActionResource_ReadDeferral(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_9_0),
tfversion.SkipIfNotPrerelease(),
tfversion.SkipIfNotAlpha(),
},
AdditionalCLIOptions: &resource.AdditionalCLIOptions{
Plan: resource.PlanOptions{AllowDeferral: true},
Expand Down Expand Up @@ -240,7 +240,7 @@ func TestDeferredActionResource_ImportStateDeferral(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_9_0),
tfversion.SkipIfNotPrerelease(),
tfversion.SkipIfNotAlpha(),
},
AdditionalCLIOptions: &resource.AdditionalCLIOptions{
Plan: resource.PlanOptions{AllowDeferral: true},
Expand Down
107 changes: 107 additions & 0 deletions internal/framework5provider/ephemeral_lifecycle_resource.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package framework

import (
"context"
"fmt"
"time"

"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/types"
)

var (
_ ephemeral.EphemeralResourceWithConfigure = &EphemeralLifecycleResource{}
_ ephemeral.EphemeralResourceWithRenew = &EphemeralLifecycleResource{}
_ ephemeral.EphemeralResourceWithClose = &EphemeralLifecycleResource{}
)

func NewEphemeralLifecycleResource() ephemeral.EphemeralResource {
return &EphemeralLifecycleResource{}
}

// EphemeralLifecycleResource is for testing the ephemeral resource lifecycle (Open, Renew, Close)
type EphemeralLifecycleResource struct {
spyClient *EphemeralResourceSpyClient
}

type EphemeralLifecycleResourceModel struct {
Name types.String `tfsdk:"name"`
Token types.String `tfsdk:"token"`
}

func (e *EphemeralLifecycleResource) Metadata(ctx context.Context, req ephemeral.MetadataRequest, resp *ephemeral.MetadataResponse) {
resp.TypeName = req.ProviderTypeName + "_lifecycle"
}

func (e *EphemeralLifecycleResource) Schema(ctx context.Context, req ephemeral.SchemaRequest, resp *ephemeral.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"name": schema.StringAttribute{
Required: true,
},
"token": schema.StringAttribute{
Computed: true,
},
},
}
}

func (e *EphemeralLifecycleResource) Configure(ctx context.Context, req ephemeral.ConfigureRequest, resp *ephemeral.ConfigureResponse) {
if req.ProviderData == nil {
return
}

spyClient, ok := req.ProviderData.(*EphemeralResourceSpyClient)

if !ok {
resp.Diagnostics.AddError(
"Unexpected Ephemeral Resource Configure Type",
fmt.Sprintf("Expected *EphemeralResourceSpyClient, got: %T.", req.ProviderData),
)

return
}

e.spyClient = spyClient
}

func (e *EphemeralLifecycleResource) Open(ctx context.Context, req ephemeral.OpenRequest, resp *ephemeral.OpenResponse) {
var data EphemeralLifecycleResourceModel

resp.Diagnostics.Append(req.Config.Get(ctx, &data)...)
if resp.Diagnostics.HasError() {
return
}

if data.Name.IsUnknown() {
resp.Diagnostics.AddAttributeError(
path.Root("name"),
"Unknown value encountered in Open lifecycle handler",
`The "name" attribute should never be unknown, Terraform core should skip executing the Open lifecycle handler until the value becomes known.`,
)
return
}

data.Token = types.StringValue("fake-token-12345")

// Renew in 5 seconds
resp.RenewAt = time.Now().Add(5 * time.Second)

resp.Diagnostics.Append(resp.Result.Set(ctx, &data)...)
}

func (e *EphemeralLifecycleResource) Renew(ctx context.Context, req ephemeral.RenewRequest, resp *ephemeral.RenewResponse) {
e.spyClient.Renew()

// Renew again in 5 seconds
resp.RenewAt = time.Now().Add(5 * time.Second)
}

func (e *EphemeralLifecycleResource) Close(ctx context.Context, req ephemeral.CloseRequest, resp *ephemeral.CloseResponse) {
e.spyClient.Close()
}
121 changes: 121 additions & 0 deletions internal/framework5provider/ephemeral_lifecycle_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package framework

import (
"fmt"
"regexp"
"testing"

"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"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/terraform"
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

// This test is a smoke test for the ephemeral resource lifecycle (Open, Renew, and Close).
func TestEphemeralLifecycleResource_basic(t *testing.T) {
t.Parallel()

spyClient := &EphemeralResourceSpyClient{}
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{
"time": {
Source: "hashicorp/time",
},
},
ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
"framework": providerserver.NewProtocol5WithError(NewWithEphemeralSpy(spyClient)),
},
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"echo": echoprovider.NewProviderServer(),
},
Steps: []resource.TestStep{
{
Config: `
ephemeral "framework_lifecycle" "test" {
name = "John Doe"
}
resource "time_sleep" "wait_20_seconds" {
depends_on = [ephemeral.framework_lifecycle.test]
create_duration = "20s"
}`,
},
},
CheckDestroy: func(_ *terraform.State) error {
// We only really care that renew was being invoked multiple times, it should always be 4 invocations (with no skew), but we'll give a little leeway here.
if spyClient.RenewInvocations() < 3 {
t.Errorf("Renew lifecycle handler should have been executed at least 3 times (5s renewals in 20s), but was only executed %d times", spyClient.RenewInvocations())
}

// Close will be invoked 6 times (due to all of the planning/refreshing of the testing framework), but we only care that it was executed once.
if spyClient.CloseInvocations() < 1 {
t.Errorf("Close lifecycle handler should have been executed at least once")
}

return nil
},
})
}

// This test ensures that Terraform will skip invoking an ephemeral resource when unknown values are present in configuration.
// The framework_lifecycle ephemeral resource will return a diagnostic if an unknown value is encountered in "name".
func TestEphemeralLifecycleResource_SkipWithUnknown(t *testing.T) {
t.Parallel()

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{
"random": {
Source: "hashicorp/random",
},
},
ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
"framework": providerserver.NewProtocol5WithError(New()),
},
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"echo": echoprovider.NewProviderServer(),
},
Steps: []resource.TestStep{
{
Config: addEchoToEphemeralLifecycleConfig(`
resource "random_string" "str" {
length = 12
}

ephemeral "framework_lifecycle" "test" {
name = "John ${random_string.str.result}"
}`),
// TODO: This is currently a known bug in Terraform v1.10.0-beta1. Once that bug is fixed, this test will fail, and then
// we can remove the ExpectError and uncomment the state checks.
ExpectError: regexp.MustCompile(`Unknown value encountered in Open lifecycle handler`),
// ConfigStateChecks: []statecheck.StateCheck{
// statecheck.ExpectKnownValue("echo.lifecycle_test", echoDataPath.AtMapKey("name"), knownvalue.StringRegexp(regexp.MustCompile(`^John\s.{12}$`))),
// statecheck.ExpectKnownValue("echo.lifecycle_test", echoDataPath.AtMapKey("token"), knownvalue.StringExact("fake-token-12345")),
// },
},
},
})
}

// Adds the test echo provider to enable using state checks with ephemeral resources
func addEchoToEphemeralLifecycleConfig(cfg string) string {
return fmt.Sprintf(`
%s
provider "echo" {
data = ephemeral.framework_lifecycle.test
}
resource "echo" "lifecycle_test" {}
`, cfg)
}
30 changes: 30 additions & 0 deletions internal/framework5provider/ephemeral_resource_spy_client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package framework

// EphemeralResourceSpyClient is used in tests to verify that an ephemeral resource lifecycle handler has been executed.
type EphemeralResourceSpyClient struct {
renewInvocations int
closeInvocations int
}

// Renew will increment the number of invocations for this instance, which can be retrieved with the `RenewInvocations` method
func (e *EphemeralResourceSpyClient) Renew() {
e.renewInvocations++
}

// RenewInvocations returns the number of times the `Renew` method has been called on this instance.
func (e *EphemeralResourceSpyClient) RenewInvocations() int {
return e.renewInvocations
}

// Close will increment the number of invocations for this instance, which can be retrieved with the `CloseInvocations` method
func (e *EphemeralResourceSpyClient) Close() {
e.closeInvocations++
}

// CloseInvocations returns the number of times the `Close` method has been called on this instance.
func (e *EphemeralResourceSpyClient) CloseInvocations() int {
return e.closeInvocations
}
26 changes: 23 additions & 3 deletions internal/framework5provider/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"context"

"github.com/hashicorp/terraform-plugin-framework/datasource"
"github.com/hashicorp/terraform-plugin-framework/ephemeral"
"github.com/hashicorp/terraform-plugin-framework/function"
"github.com/hashicorp/terraform-plugin-framework/provider"
"github.com/hashicorp/terraform-plugin-framework/provider/schema"
Expand All @@ -17,14 +18,25 @@ import (
)

var (
_ provider.ProviderWithFunctions = (*testProvider)(nil)
_ provider.ProviderWithFunctions = (*testProvider)(nil)
_ provider.ProviderWithEphemeralResources = (*testProvider)(nil)
)

func New() provider.Provider {
return &testProvider{}
return &testProvider{
ephSpyClient: &EphemeralResourceSpyClient{},
}
}

func NewWithEphemeralSpy(spy *EphemeralResourceSpyClient) provider.Provider {
return &testProvider{
ephSpyClient: spy,
}
}

type testProvider struct{}
type testProvider struct {
ephSpyClient *EphemeralResourceSpyClient
}

func (p *testProvider) Metadata(_ context.Context, _ provider.MetadataRequest, resp *provider.MetadataResponse) {
resp.TypeName = "framework"
Expand Down Expand Up @@ -61,6 +73,7 @@ func (p *testProvider) Configure(ctx context.Context, req provider.ConfigureRequ
}
}
resp.ResourceData = client
resp.EphemeralResourceData = p.ephSpyClient
}

func (p *testProvider) Resources(_ context.Context) []func() resource.Resource {
Expand Down Expand Up @@ -105,6 +118,13 @@ func (p *testProvider) Functions(ctx context.Context) []func() function.Function
}
}

func (p *testProvider) EphemeralResources(ctx context.Context) []func() ephemeral.EphemeralResource {
return []func() ephemeral.EphemeralResource{
NewSchemaEphemeralResource,
NewEphemeralLifecycleResource,
}
}

type providerConfig struct {
Dummy types.String `tfsdk:"dummy"`
Deferral types.Bool `tfsdk:"deferral"`
Expand Down
Loading
Loading