Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@ go 1.23.7
require (
github.com/hashicorp/go-cty v1.5.0
github.com/hashicorp/go-memdb v1.3.5
github.com/hashicorp/go-version v1.7.0
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab
github.com/hashicorp/terraform-plugin-framework v1.15.0-beta.1
github.com/hashicorp/terraform-plugin-framework v1.15.0-beta.1.0.20250512133431-c61b1b9a48d4
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0
github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0
github.com/hashicorp/terraform-plugin-framework-timeouts v0.5.0
Expand Down Expand Up @@ -37,6 +36,7 @@ require (
github.com/hashicorp/go-plugin v1.6.3 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/hashicorp/go-uuid v1.0.3 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hc-install v0.9.2 // indirect
github.com/hashicorp/hcl/v2 v2.23.0 // indirect
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ github.com/hashicorp/terraform-exec v0.23.0 h1:MUiBM1s0CNlRFsCLJuM5wXZrzA3MnPYEs
github.com/hashicorp/terraform-exec v0.23.0/go.mod h1:mA+qnx1R8eePycfwKkCRk3Wy65mwInvlpAeOwmA7vlY=
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab h1:5Qpuprk76zkVEdTCtfoPjUc+1AeUxlgkF6sWTr7qLDs=
github.com/hashicorp/terraform-json v0.24.1-0.20250314103308-f86d5e36f4ab/go.mod h1:sMKS8fiRDX4rVlR6EJUMudg1WcanxCMoWwTLkgZP/vc=
github.com/hashicorp/terraform-plugin-framework v1.15.0-beta.1 h1:lX4qacaJc8dqUzEaOALeUW0Gvv0ACs9myvN1WQ4rRgU=
github.com/hashicorp/terraform-plugin-framework v1.15.0-beta.1/go.mod h1:SNnBQzWTh3ydNHBJF8eLVHlm/2gu+RBG508LCfCSVwI=
github.com/hashicorp/terraform-plugin-framework v1.15.0-beta.1.0.20250512133431-c61b1b9a48d4 h1:bQtTmUaSW3UUd/UqB6i5YvGGKY/uh1NxfiLPmpp12gY=
github.com/hashicorp/terraform-plugin-framework v1.15.0-beta.1.0.20250512133431-c61b1b9a48d4/go.mod h1:ZlVO3Sv2z0tnLP+f3/30hOFwaiM/BNye0H8HFRN1nDo=
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0 h1:SJXL5FfJJm17554Kpt9jFXngdM6fXbnUnZ6iT2IeiYA=
github.com/hashicorp/terraform-plugin-framework-jsontypes v0.2.0/go.mod h1:p0phD0IYhsu9bR4+6OetVvvH59I6LwjXGnTVEr8ox6E=
github.com/hashicorp/terraform-plugin-framework-nettypes v0.2.0 h1:Zap24rkky7SvNGGNYHMKFhAriP6+6riI21BMYOYgLRE=
Expand Down
35 changes: 33 additions & 2 deletions internal/framework5provider/identity_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import (
"context"
"fmt"

"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
"github.com/hashicorp/terraform-plugin-framework/types"
)

var _ resource.Resource = IdentityResource{}
var _ resource.ResourceWithIdentity = IdentityResource{}
var _ resource.ResourceWithImportState = IdentityResource{}

func NewIdentityResource() resource.Resource {
return &IdentityResource{}
Expand Down Expand Up @@ -42,13 +46,23 @@ func (r IdentityResource) Metadata(_ context.Context, req resource.MetadataReque
func (r IdentityResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
resp.Schema = schema.Schema{
Attributes: map[string]schema.Attribute{
"id": schema.StringAttribute{
Computed: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.UseStateForUnknown(),
},
},
"name": schema.StringAttribute{
Required: true,
},
},
}
}

func (r IdentityResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughWithIdentity(ctx, path.Root("id"), path.Root("id"), req, resp)
}

func (r IdentityResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data IdentityResourceModel

Expand All @@ -59,20 +73,36 @@ func (r IdentityResource) Create(ctx context.Context, req resource.CreateRequest

resp.Diagnostics.Append(resp.Identity.Set(ctx, IdentityResourceIdentityModel{
ID: types.StringValue("id-123"),
Name: types.StringValue(fmt.Sprintf("my name is %s", data.Name.ValueString())),
Name: types.StringValue(data.Name.ValueString()),
})...)

data.ID = types.StringValue("id-123")
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
}

func (r IdentityResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
var data IdentityResourceModel

resp.Diagnostics.Append(req.State.Get(ctx, &data)...)

if resp.Diagnostics.HasError() {
return
}

if data.ID.ValueString() != "id-123" {
resp.Diagnostics.AddAttributeError(
path.Root("id"),
"Unexpected ID value",
fmt.Sprintf("Expected ID to be \"id-123\", got: %s", data.ID.String()),
)
return
}

data.Name = types.StringValue("tom")
resp.Diagnostics.Append(resp.Identity.Set(ctx, IdentityResourceIdentityModel{
ID: types.StringValue("id-123"),
Name: types.StringValue("tom"),
})...)

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

Expand All @@ -92,6 +122,7 @@ func (r IdentityResource) Delete(ctx context.Context, req resource.DeleteRequest
}

type IdentityResourceModel struct {
ID types.String `tfsdk:"id"`
Name types.String `tfsdk:"name"`
}

Expand Down
57 changes: 41 additions & 16 deletions internal/framework5provider/identity_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,53 +4,78 @@
package framework

import (
"regexp"
"testing"

"github.com/hashicorp/go-version"
"github.com/hashicorp/terraform-plugin-framework/providerserver"
"github.com/hashicorp/terraform-plugin-go/tfprotov5"
"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 TestIdentityResource(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(version.Must(version.NewVersion("1.12.0-beta1"))),
tfversion.SkipBelow(tfversion.Version1_12_0),
},
ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
"framework": providerserver.NewProtocol5WithError(New()),
},
Steps: []resource.TestStep{
{
Config: `resource "framework_identity" "test" {
name = "john"
name = "tom"
}`,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectIdentity("framework_identity.test", map[string]knownvalue.Check{
"id": knownvalue.StringExact("id-123"),
"name": knownvalue.StringExact("my name is john"),
"name": knownvalue.StringExact("tom"),
}),
statecheck.ExpectKnownValue("framework_identity.test", tfjsonpath.New("id"), knownvalue.StringExact("id-123")),
statecheck.ExpectKnownValue("framework_identity.test", tfjsonpath.New("name"), knownvalue.StringExact("tom")),
},
},
// Typically you don't need to test all of these different import methods,
// but this just a smoke test for passing state + identity data through.
{
ImportState: true,
ResourceName: "framework_identity.test",
ImportStateKind: resource.ImportCommandWithID,
},
{
ImportState: true,
ResourceName: "framework_identity.test",
ImportStateKind: resource.ImportBlockWithID,
},
{
ImportState: true,
ResourceName: "framework_identity.test",
ImportStateKind: resource.ImportBlockWithResourceIdentity,
},
},
})
}

func TestIdentityResource_identity_changes(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_12_0),
},
ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
"framework": providerserver.NewProtocol5WithError(New()),
},
Steps: []resource.TestStep{
{
Config: `resource "framework_identity" "test" {
name = "jerry"
}`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("framework_identity.test", plancheck.ResourceActionUpdate),
},
},
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectIdentity("framework_identity.test", map[string]knownvalue.Check{
"id": knownvalue.StringExact("id-123"),
"name": knownvalue.StringExact("my name is john"), // doesn't get updated, since identity should not change.
}),
},
// The resource is hardcoded to refresh with the same identity, based off the name attribute during create.
// Resources are currently not allowed to change identities at any time, so framework will return an error message
// after the post-apply refresh.
ExpectError: regexp.MustCompile(`Error: Unexpected Identity Change`),
},
},
})
Expand Down
87 changes: 76 additions & 11 deletions internal/framework5provider/move_state_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ 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/identityschema"
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
"github.com/hashicorp/terraform-plugin-framework/types"
"github.com/hashicorp/terraform-plugin-go/tftypes"
)

var _ resource.Resource = MoveStateResource{}
var _ resource.ResourceWithIdentity = MoveStateResource{}
var _ resource.ResourceWithMoveState = MoveStateResource{}

func NewMoveStateResource() resource.Resource {
Expand All @@ -39,6 +41,16 @@ func (r MoveStateResource) Schema(_ context.Context, _ resource.SchemaRequest, r
}
}

func (r MoveStateResource) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) {
resp.IdentitySchema = identityschema.Schema{
Attributes: map[string]identityschema.Attribute{
"id": identityschema.StringAttribute{
RequiredForImport: true,
},
},
}
}

func (r MoveStateResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) {
var data MoveStateResourceModel

Expand Down Expand Up @@ -87,25 +99,78 @@ func (r MoveStateResource) MoveState(ctx context.Context) []resource.StateMover
},
},
StateMover: func(ctx context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) {
if !strings.HasSuffix(req.SourceProviderAddress, "hashicorp/random") || req.SourceTypeName != "random_string" {
switch req.SourceProviderAddress {
case "registry.terraform.io/hashicorp/random": // Random provider (testing state moves)
if req.SourceTypeName != "random_string" {
resp.Diagnostics.AddError(
"Invalid Move State Request",
fmt.Sprintf("The \"framework_move_state\" resource can only be sourced from the \"random_string\" or \"framework_identity\" managed resources:\n\n"+
"req.SourceProviderAddress: %q\n"+
"req.SourceTypeName: %q\n",
req.SourceProviderAddress,
req.SourceTypeName,
),
)
return
}

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)...)
case "registry.terraform.io/hashicorp/framework": // Corner provider (testing identity moves)
if req.SourceTypeName != "framework_identity" {
resp.Diagnostics.AddError(
"Invalid Move State Request",
fmt.Sprintf("The \"framework_move_state\" resource can only be sourced from the \"random_string\" or \"framework_identity\" managed resources:\n\n"+
"req.SourceProviderAddress: %q\n"+
"req.SourceTypeName: %q\n",
req.SourceProviderAddress,
req.SourceTypeName,
),
)
return
}

oldIdentityVal, err := req.SourceIdentity.Unmarshal(
tftypes.Object{
AttributeTypes: map[string]tftypes.Type{
"id": tftypes.String,
"name": tftypes.String,
},
},
)
if err != nil {
resp.Diagnostics.AddError(
"Unexpected Move State Error",
fmt.Sprintf("Error decoding source identity: %s", err.Error()),
)
return
}

var sourceIdentityObj map[string]tftypes.Value
var sourceID, sourceName string

oldIdentityVal.As(&sourceIdentityObj) //nolint:errcheck // This is just a quick test of grabbing raw identity data
sourceIdentityObj["id"].As(&sourceID) //nolint:errcheck // This is just a quick test of grabbing raw identity data
sourceIdentityObj["name"].As(&sourceName) //nolint:errcheck // This is just a quick test of grabbing raw identity data

resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("moved_random_string"), sourceName)...)
resp.Diagnostics.Append(resp.TargetIdentity.SetAttribute(ctx, path.Root("id"), sourceID)...)
default:
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"+
fmt.Sprintf("This test can only migrate resource state from hardcoded provider/resource types:\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)...)
},
},
}
Expand Down
46 changes: 46 additions & 0 deletions internal/framework5provider/move_state_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"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/knownvalue"
"github.com/hashicorp/terraform-plugin-testing/plancheck"
"github.com/hashicorp/terraform-plugin-testing/statecheck"
"github.com/hashicorp/terraform-plugin-testing/tfjsonpath"
Expand Down Expand Up @@ -66,3 +67,48 @@ func TestMoveStateResource(t *testing.T) {
},
})
}

func TestMoveStateResource_identity(t *testing.T) {
resource.UnitTest(t, resource.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_12_0),
},
ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){
"framework": providerserver.NewProtocol5WithError(New()),
},
Steps: []resource.TestStep{
{
Config: `resource "framework_identity" "old" {
name = "tom"
}`,
ConfigStateChecks: []statecheck.StateCheck{
statecheck.ExpectIdentity("framework_identity.old", map[string]knownvalue.Check{
"id": knownvalue.StringExact("id-123"),
"name": knownvalue.StringExact("tom"),
}),
},
},
{
Config: `
moved {
from = framework_identity.old
to = framework_move_state.new
}
resource "framework_move_state" "new" {}
`,
ConfigPlanChecks: resource.ConfigPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectEmptyPlan(),
},
},
ConfigStateChecks: []statecheck.StateCheck{
// The previous framework_identity.old identity should be moved to this new location, split into the new location identity and state.
statecheck.ExpectIdentity("framework_move_state.new", map[string]knownvalue.Check{
"id": knownvalue.StringExact("id-123"),
}),
statecheck.ExpectKnownValue("framework_move_state.new", tfjsonpath.New("moved_random_string"), knownvalue.StringExact("tom")),
},
},
},
})
}
Loading
Loading