diff --git a/internal/framework5provider/move_state_resource.go b/internal/framework5provider/move_state_resource.go new file mode 100644 index 0000000..5d60fd6 --- /dev/null +++ b/internal/framework5provider/move_state_resource.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = MoveStateResource{} +var _ resource.ResourceWithMoveState = MoveStateResource{} + +func NewMoveStateResource() resource.Resource { + return &MoveStateResource{} +} + +// MoveStateResource is for testing the MoveResourceState RPC +// https://developer.hashicorp.com/terraform/plugin/framework/resources/state-move +type MoveStateResource struct{} + +func (r MoveStateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_move_state" +} + +func (r MoveStateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "moved_random_string": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (r MoveStateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MoveStateResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r MoveStateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data MoveStateResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r MoveStateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data MoveStateResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r MoveStateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +func (r MoveStateResource) MoveState(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + SourceSchema: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "result": schema.StringAttribute{}, + }, + }, + StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + if !strings.HasSuffix(req.SourceProviderAddress, "hashicorp/random") || req.SourceTypeName != "random_string" { + resp.Diagnostics.AddError( + "Invalid Move State Request", + fmt.Sprintf("This test can only migrate resource state from the \"random_string\" managed resource from the \"hashicorp/random\" provider:\n\n"+ + "req.SourceProviderAddress: %q\n"+ + "req.SourceTypeName: %q\n", + req.SourceProviderAddress, + req.SourceTypeName, + ), + ) + } + + var oldState RandomStringResourceModel + resp.Diagnostics.Append(req.SourceState.Get(ctx, &oldState)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("moved_random_string"), oldState.Result)...) + }, + }, + } +} + +// https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string +type RandomStringResourceModel struct { + Result types.String `tfsdk:"result"` +} + +type MoveStateResourceModel struct { + MovedRandomString types.String `tfsdk:"moved_random_string"` +} diff --git a/internal/framework5provider/move_state_resource_test.go b/internal/framework5provider/move_state_resource_test.go new file mode 100644 index 0000000..4258f93 --- /dev/null +++ b/internal/framework5provider/move_state_resource_test.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "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" +) + +// This is a smoke test for using the "moved" block to transition state between +// the "random_string" managed resource and the corner provider "framework_move_state" +// managed resource. +// +// Ref: https://github.com/hashicorp/terraform-plugin-framework/issues/1039 +func TestMoveStateResource(t *testing.T) { + randomStringSame := statecheck.CompareValue(compare.ValuesSame()) + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_8_0), + }, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": { + Source: "hashicorp/random", + }, + }, + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "framework": providerserver.NewProtocol5WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "random_string" "old" { + length = 12 + }`, + ConfigStateChecks: []statecheck.StateCheck{ + randomStringSame.AddStateValue("random_string.old", tfjsonpath.New("result")), + }, + }, + { + Config: ` + moved { + from = random_string.old + to = framework_move_state.new + } + resource "framework_move_state" "new" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + // The previous random_string.result value should be moved to this new location unchanged. + randomStringSame.AddStateValue("framework_move_state.new", tfjsonpath.New("moved_random_string")), + }, + }, + }, + }) +} diff --git a/internal/framework5provider/provider.go b/internal/framework5provider/provider.go index dffd2cd..c1c6de9 100644 --- a/internal/framework5provider/provider.go +++ b/internal/framework5provider/provider.go @@ -76,6 +76,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewFloat32PrecisionResource, NewFloat64PrecisionResource, NewTFSDKReflectionResource, + NewMoveStateResource, NewSetNestedBlockWithDefaultsResource, } } diff --git a/internal/framework6provider/move_state_resource.go b/internal/framework6provider/move_state_resource.go new file mode 100644 index 0000000..5d60fd6 --- /dev/null +++ b/internal/framework6provider/move_state_resource.go @@ -0,0 +1,121 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +var _ resource.Resource = MoveStateResource{} +var _ resource.ResourceWithMoveState = MoveStateResource{} + +func NewMoveStateResource() resource.Resource { + return &MoveStateResource{} +} + +// MoveStateResource is for testing the MoveResourceState RPC +// https://developer.hashicorp.com/terraform/plugin/framework/resources/state-move +type MoveStateResource struct{} + +func (r MoveStateResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_move_state" +} + +func (r MoveStateResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "moved_random_string": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (r MoveStateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data MoveStateResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r MoveStateResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data MoveStateResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r MoveStateResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data MoveStateResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r MoveStateResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +func (r MoveStateResource) MoveState(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + SourceSchema: &schema.Schema{ + Attributes: map[string]schema.Attribute{ + "result": schema.StringAttribute{}, + }, + }, + StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + if !strings.HasSuffix(req.SourceProviderAddress, "hashicorp/random") || req.SourceTypeName != "random_string" { + resp.Diagnostics.AddError( + "Invalid Move State Request", + fmt.Sprintf("This test can only migrate resource state from the \"random_string\" managed resource from the \"hashicorp/random\" provider:\n\n"+ + "req.SourceProviderAddress: %q\n"+ + "req.SourceTypeName: %q\n", + req.SourceProviderAddress, + req.SourceTypeName, + ), + ) + } + + var oldState RandomStringResourceModel + resp.Diagnostics.Append(req.SourceState.Get(ctx, &oldState)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("moved_random_string"), oldState.Result)...) + }, + }, + } +} + +// https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/string +type RandomStringResourceModel struct { + Result types.String `tfsdk:"result"` +} + +type MoveStateResourceModel struct { + MovedRandomString types.String `tfsdk:"moved_random_string"` +} diff --git a/internal/framework6provider/move_state_resource_test.go b/internal/framework6provider/move_state_resource_test.go new file mode 100644 index 0000000..3404f12 --- /dev/null +++ b/internal/framework6provider/move_state_resource_test.go @@ -0,0 +1,68 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package framework + +import ( + "testing" + + "github.com/hashicorp/terraform-plugin-framework/providerserver" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-testing/compare" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "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" +) + +// This is a smoke test for using the "moved" block to transition state between +// the "random_string" managed resource and the corner provider "framework_move_state" +// managed resource. +// +// Ref: https://github.com/hashicorp/terraform-plugin-framework/issues/1039 +func TestMoveStateResource(t *testing.T) { + randomStringSame := statecheck.CompareValue(compare.ValuesSame()) + + resource.UnitTest(t, resource.TestCase{ + TerraformVersionChecks: []tfversion.TerraformVersionCheck{ + tfversion.SkipBelow(tfversion.Version1_8_0), + }, + ExternalProviders: map[string]resource.ExternalProvider{ + "random": { + Source: "hashicorp/random", + }, + }, + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "framework": providerserver.NewProtocol6WithError(New()), + }, + Steps: []resource.TestStep{ + { + Config: `resource "random_string" "old" { + length = 12 + }`, + ConfigStateChecks: []statecheck.StateCheck{ + randomStringSame.AddStateValue("random_string.old", tfjsonpath.New("result")), + }, + }, + { + Config: ` + moved { + from = random_string.old + to = framework_move_state.new + } + resource "framework_move_state" "new" {} + `, + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectEmptyPlan(), + }, + }, + ConfigStateChecks: []statecheck.StateCheck{ + // The previous random_string.result value should be moved to this new location unchanged. + randomStringSame.AddStateValue("framework_move_state.new", tfjsonpath.New("moved_random_string")), + }, + }, + }, + }) +} diff --git a/internal/framework6provider/provider.go b/internal/framework6provider/provider.go index b8bd08a..ffdd67f 100644 --- a/internal/framework6provider/provider.go +++ b/internal/framework6provider/provider.go @@ -75,6 +75,7 @@ func (p *testProvider) Resources(_ context.Context) []func() resource.Resource { NewFloat32PrecisionResource, NewFloat64PrecisionResource, NewTFSDKReflectionResource, + NewMoveStateResource, NewSetNestedBlockWithDefaultsResource, NewSetNestedAttributeWithDefaultsResource, }