Skip to content

Commit

Permalink
all: Add ephemeral resource schema and lifecycle tests (#283)
Browse files Browse the repository at this point in the history
* echo resource

* ephemeral resource with tests

* move echo provider to plugin testing

* update with testing echo provider

* switch to constants

* constants

* rename files

* added basic ephemeral schema test

* add protov6 ephemeral resource test

* receiver variable

* add lifecycle tests

* add nested attributes + dynamics to ephemeral schema test

* update interface checks

* add copyright headers

* update resource type name

* use alpha skip for deferred action tests

* update plugin testing dep

* update to latest testing commit with new server behavior

* switch to using depends_on

* share the data path from a variable

* adjust the newly fixed RC1 tests + adjust test assertion order
  • Loading branch information
austinvalle authored Nov 19, 2024
1 parent f9d3212 commit 384a2a9
Show file tree
Hide file tree
Showing 15 changed files with 2,929 additions and 18 deletions.
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()
}
120 changes: 120 additions & 0 deletions internal/framework5provider/ephemeral_lifecycle_resource_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
// 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/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"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}"
}`),
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

0 comments on commit 384a2a9

Please sign in to comment.