diff --git a/internal/fromproto5/moveresourcestate.go b/internal/fromproto5/moveresourcestate.go index f5e836b7..988bb0ab 100644 --- a/internal/fromproto5/moveresourcestate.go +++ b/internal/fromproto5/moveresourcestate.go @@ -5,7 +5,6 @@ package fromproto5 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -17,7 +16,7 @@ import ( // MoveResourceStateRequest returns the *fwserver.MoveResourceStateRequest // equivalent of a *tfprotov5.MoveResourceStateRequest. -func MoveResourceStateRequest(ctx context.Context, proto5 *tfprotov5.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { +func MoveResourceStateRequest(ctx context.Context, proto5 *tfprotov5.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -38,13 +37,16 @@ func MoveResourceStateRequest(ctx context.Context, proto5 *tfprotov5.MoveResourc } fw := &fwserver.MoveResourceStateRequest{ - SourceProviderAddress: proto5.SourceProviderAddress, - SourceRawState: (*tfprotov6.RawState)(proto5.SourceState), - SourceSchemaVersion: proto5.SourceSchemaVersion, - SourceTypeName: proto5.SourceTypeName, - TargetResource: resource, - TargetResourceSchema: resourceSchema, - TargetTypeName: proto5.TargetTypeName, + SourceProviderAddress: proto5.SourceProviderAddress, + SourceRawState: (*tfprotov6.RawState)(proto5.SourceState), + SourceSchemaVersion: proto5.SourceSchemaVersion, + SourceTypeName: proto5.SourceTypeName, + TargetResource: resource, + TargetResourceSchema: resourceSchema, + TargetTypeName: proto5.TargetTypeName, + SourceIdentity: (*tfprotov6.RawState)(proto5.SourceIdentity), + SourceIdentitySchemaVersion: proto5.SourceIdentitySchemaVersion, + IdentitySchema: identitySchema, } sourcePrivate, sourcePrivateDiags := privatestate.NewData(ctx, proto5.SourcePrivate) diff --git a/internal/fromproto5/moveresourcestate_test.go b/internal/fromproto5/moveresourcestate_test.go index 2122edb7..78182cd0 100644 --- a/internal/fromproto5/moveresourcestate_test.go +++ b/internal/fromproto5/moveresourcestate_test.go @@ -32,6 +32,7 @@ func TestMoveResourceStateRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.MoveResourceStateRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource expected *fwserver.MoveResourceStateRequest expectedDiagnostics diag.Diagnostics @@ -162,13 +163,37 @@ func TestMoveResourceStateRequest(t *testing.T) { TargetTypeName: "examplecloud_thing", }, }, + "SourceIdentity": { + input: &tfprotov5.MoveResourceStateRequest{ + SourceIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentity: testNewTfprotov6RawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + TargetResourceSchema: testFwSchema, + }, + }, + "SourceIdentitySchemaVersion": { + input: &tfprotov5.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + TargetResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema) + got, diags := fromproto5.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/moveresourcestate.go b/internal/fromproto6/moveresourcestate.go index 6b31a2cc..7e85ce9f 100644 --- a/internal/fromproto6/moveresourcestate.go +++ b/internal/fromproto6/moveresourcestate.go @@ -5,7 +5,6 @@ package fromproto6 import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" @@ -16,7 +15,7 @@ import ( // MoveResourceStateRequest returns the *fwserver.MoveResourceStateRequest // equivalent of a *tfprotov6.MoveResourceStateRequest. -func MoveResourceStateRequest(ctx context.Context, proto6 *tfprotov6.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { +func MoveResourceStateRequest(ctx context.Context, proto6 *tfprotov6.MoveResourceStateRequest, resource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.MoveResourceStateRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -37,13 +36,16 @@ func MoveResourceStateRequest(ctx context.Context, proto6 *tfprotov6.MoveResourc } fw := &fwserver.MoveResourceStateRequest{ - SourceProviderAddress: proto6.SourceProviderAddress, - SourceRawState: proto6.SourceState, - SourceSchemaVersion: proto6.SourceSchemaVersion, - SourceTypeName: proto6.SourceTypeName, - TargetResource: resource, - TargetResourceSchema: resourceSchema, - TargetTypeName: proto6.TargetTypeName, + SourceProviderAddress: proto6.SourceProviderAddress, + SourceRawState: proto6.SourceState, + SourceSchemaVersion: proto6.SourceSchemaVersion, + SourceTypeName: proto6.SourceTypeName, + TargetResource: resource, + TargetResourceSchema: resourceSchema, + TargetTypeName: proto6.TargetTypeName, + SourceIdentity: proto6.SourceIdentity, + SourceIdentitySchemaVersion: proto6.SourceIdentitySchemaVersion, + IdentitySchema: identitySchema, } sourcePrivate, sourcePrivateDiags := privatestate.NewData(ctx, proto6.SourcePrivate) diff --git a/internal/fromproto6/moveresourcestate_test.go b/internal/fromproto6/moveresourcestate_test.go index 881ab3a4..d53449a7 100644 --- a/internal/fromproto6/moveresourcestate_test.go +++ b/internal/fromproto6/moveresourcestate_test.go @@ -32,6 +32,7 @@ func TestMoveResourceStateRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.MoveResourceStateRequest resourceSchema fwschema.Schema + identitySchema fwschema.Schema resource resource.Resource expected *fwserver.MoveResourceStateRequest expectedDiagnostics diag.Diagnostics @@ -162,13 +163,37 @@ func TestMoveResourceStateRequest(t *testing.T) { TargetTypeName: "examplecloud_thing", }, }, + "SourceIdentity": { + input: &tfprotov6.MoveResourceStateRequest{ + SourceIdentity: testNewRawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentity: testNewRawState(t, map[string]interface{}{ + "test_identity_attribute": "test-value", + }), + TargetResourceSchema: testFwSchema, + }, + }, + "SourceIdentitySchemaVersion": { + input: &tfprotov6.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + }, + resourceSchema: testFwSchema, + expected: &fwserver.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + TargetResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema) + got, diags := fromproto6.MoveResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fwserver/server_moveresourcestate.go b/internal/fwserver/server_moveresourcestate.go index 1a832f3f..b838687d 100644 --- a/internal/fwserver/server_moveresourcestate.go +++ b/internal/fwserver/server_moveresourcestate.go @@ -65,14 +65,25 @@ type MoveResourceStateRequest struct { // TargetTypeName is the type name of the target resource as given by // Terraform across the protocol. TargetTypeName string + + // SourceIdentity is the identity of the source resource. + // + // Only the underlying JSON field is populated. + SourceIdentity *tfprotov6.RawState + + // SourceIdentitySchemaVersion is the version of the source resource state. + SourceIdentitySchemaVersion int64 + + IdentitySchema fwschema.Schema } // MoveResourceStateResponse is the framework server response for the // MoveResourceState RPC. type MoveResourceStateResponse struct { - Diagnostics diag.Diagnostics - TargetPrivate *privatestate.Data - TargetState *tfsdk.State + Diagnostics diag.Diagnostics + TargetPrivate *privatestate.Data + TargetState *tfsdk.State + TargetIdentity *tfsdk.ResourceIdentity } // MoveResourceState implements the framework server MoveResourceState RPC. @@ -125,12 +136,15 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe for _, resourceStateMover := range resourceStateMovers { moveStateReq := resource.MoveStateRequest{ - SourcePrivate: sourcePrivate, - SourceProviderAddress: req.SourceProviderAddress, - SourceRawState: req.SourceRawState, - SourceSchemaVersion: req.SourceSchemaVersion, - SourceTypeName: req.SourceTypeName, + SourcePrivate: sourcePrivate, + SourceProviderAddress: req.SourceProviderAddress, + SourceRawState: req.SourceRawState, + SourceSchemaVersion: req.SourceSchemaVersion, + SourceTypeName: req.SourceTypeName, + SourceIdentity: req.SourceIdentity, + SourceIdentitySchemaVersion: req.SourceIdentitySchemaVersion, } + moveStateResp := resource.MoveStateResponse{ TargetPrivate: privatestate.EmptyProviderData(ctx), TargetState: tfsdk.State{ @@ -139,6 +153,13 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe }, } + if req.IdentitySchema != nil { + moveStateResp.TargetIdentity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil), + Schema: req.IdentitySchema, + } + } + if resourceStateMover.SourceSchema != nil { logging.FrameworkTrace(ctx, "Attempting to populate MoveResourceStateRequest SourceState from provider defined SourceSchema") @@ -221,6 +242,19 @@ func (s *Server) MoveResourceState(ctx context.Context, req *MoveResourceStateRe if !moveStateResp.TargetState.Raw.Equal(tftypes.NewValue(req.TargetResourceSchema.Type().TerraformType(ctx), nil)) { resp.Diagnostics = moveStateResp.Diagnostics resp.TargetState = &moveStateResp.TargetState + if moveStateResp.TargetIdentity != nil { + resp.TargetIdentity = moveStateResp.TargetIdentity + } + + if resp.TargetIdentity != nil && req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Move State Response", + "An unexpected error was encountered when creating the move state response. New identity data was returned by the provider move state operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ) + + return + } if moveStateResp.TargetPrivate != nil { resp.TargetPrivate.Provider = moveStateResp.TargetPrivate diff --git a/internal/fwserver/server_moveresourcestate_test.go b/internal/fwserver/server_moveresourcestate_test.go index 8e2445ba..90e32aeb 100644 --- a/internal/fwserver/server_moveresourcestate_test.go +++ b/internal/fwserver/server_moveresourcestate_test.go @@ -6,6 +6,7 @@ package fwserver_test import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "testing" "github.com/google/go-cmp/cmp" @@ -39,8 +40,19 @@ func TestServerMoveResourceState(t *testing.T) { }, }, } + + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + schemaType := testSchema.Type().TerraformType(ctx) + schemaIdentityType := testIdentitySchema.Type().TerraformType(ctx) + testSchemaWriteOnly := schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -812,6 +824,167 @@ func TestServerMoveResourceState(t *testing.T) { }, }, }, + "request-SourceIdentitySchemaVersion": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + // SourceRawState required to prevent error + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expected := int64(123) + + if diff := cmp.Diff(req.SourceIdentitySchemaVersion, expected); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentitySchemaVersion difference", diff) + } + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchema, + TargetTypeName: "test_resource", + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + TargetPrivate: privatestate.EmptyData(ctx), + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchema, + }, + }, + }, + "request-SourceIdentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + // SourceRawState required to prevent error + SourceIdentity: testNewRawState(t, map[string]interface{}{ + "test_id": "test_id_value", + }), + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expectedSourceIdentity := testNewRawState(t, map[string]interface{}{ + "test_id": "test_id_value", + }) + + if diff := cmp.Diff(req.SourceIdentity, expectedSourceIdentity); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentity difference", diff) + } + + resp.Diagnostics.Append(resp.TargetIdentity.SetAttribute(ctx, path.Root("test_id"), "test_id_value")...) + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchema, + TargetTypeName: "test_resource", + IdentitySchema: testIdentitySchema, + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + TargetPrivate: privatestate.EmptyData(ctx), + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchema, + }, + TargetIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test_id_value"), + }), + Schema: testIdentitySchema, + }, + }, + }, + "response-invalid-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.MoveResourceStateRequest{ + SourceRawState: testNewRawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetResource: &testprovider.ResourceWithMoveState{ + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + // This resource doesn't indicate identity support (via a schema), so this should raise a diagnostic. + resp.TargetIdentity = &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchema.Type().TerraformType(ctx), map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "should-not-be-set"), + }), + Schema: testIdentitySchema, + } + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + }, + TargetResourceSchema: testSchema, + TargetTypeName: "test_resource", + }, + expectedResponse: &fwserver.MoveResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Move State Response", + "An unexpected error was encountered when creating the move state response. New identity data was returned by the provider move state operation, but the resource does not indicate identity support.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.", + ), + }, + TargetState: &tfsdk.State{ + Raw: tftypes.NewValue(schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + Schema: testSchema, + }, + TargetIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(schemaIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "should-not-be-set"), + }), + Schema: testIdentitySchema, + }, + TargetPrivate: privatestate.EmptyData(ctx), + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto5server/server_moveresourcestate.go b/internal/proto5server/server_moveresourcestate.go index efa1b118..23b570ed 100644 --- a/internal/proto5server/server_moveresourcestate.go +++ b/internal/proto5server/server_moveresourcestate.go @@ -36,11 +36,19 @@ func (s *Server) MoveResourceState(ctx context.Context, proto5Req *tfprotov5.Mov fwResp.Diagnostics.Append(diags...) + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TargetTypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.MoveResourceStateResponse(ctx, fwResp), nil + } + if fwResp.Diagnostics.HasError() { return toproto5.MoveResourceStateResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.MoveResourceStateRequest(ctx, proto5Req, resource, resourceSchema) + fwReq, diags := fromproto5.MoveResourceStateRequest(ctx, proto5Req, resource, resourceSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_moveresourcestate_test.go b/internal/proto5server/server_moveresourcestate_test.go index 5b5e53c9..17b11fc4 100644 --- a/internal/proto5server/server_moveresourcestate_test.go +++ b/internal/proto5server/server_moveresourcestate_test.go @@ -6,6 +6,7 @@ package proto5server import ( "context" "fmt" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "testing" "github.com/google/go-cmp/cmp" @@ -39,6 +40,20 @@ func TestServerMoveResourceState(t *testing.T) { } schemaType := testSchema.Type().TerraformType(ctx) + testIdentitySchema := identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + } + + testIdentityType := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + testCases := map[string]struct { server *Server request *tfprotov5.MoveResourceStateRequest @@ -413,6 +428,140 @@ func TestServerMoveResourceState(t *testing.T) { }), }, }, + "request-SourceIdentitySchemaVersion": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithMoveState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expected := int64(123) + + if diff := cmp.Diff(req.SourceIdentitySchemaVersion, expected); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentitySchemaVersion difference", diff) + } + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.MoveResourceStateRequest{ + SourceIdentitySchemaVersion: 123, + // SourceState required to prevent error + SourceState: testNewTfprotov5RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + TargetTypeName: "test_resource", + }, + expectedResponse: &tfprotov5.MoveResourceStateResponse{ + TargetState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + }, + }, + "request-SourceIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndMoveState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + expectedSourceRawState := testNewTfprotov6RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }) + + if diff := cmp.Diff(req.SourceRawState, expectedSourceRawState); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceRawState difference", diff) + } + + expectedSourceIdentity := testNewTfprotov6RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }) + + if diff := cmp.Diff(req.SourceIdentity, expectedSourceIdentity); diff != "" { + resp.Diagnostics.AddError("Unexpected req.SourceIdentity difference", diff) + } + + resp.Diagnostics.Append(resp.TargetIdentity.SetAttribute(ctx, path.Root("test_id"), "test-id-value")...) + + // Prevent missing implementation error, the values do not matter except for response assertion + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.MoveResourceStateRequest{ + SourceState: testNewTfprotov5RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + SourceIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TargetTypeName: "test_resource", + }, + expectedResponse: &tfprotov5.MoveResourceStateResponse{ + TargetIdentity: &tfprotov5.ResourceIdentityData{IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + })}, + TargetState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + }, + }, "request-TargetTypeName-missing": { server: &Server{ FrameworkServer: fwserver.Server{ @@ -705,6 +854,64 @@ func TestServerMoveResourceState(t *testing.T) { }), }, }, + "response-TargetIdentity": { + server: &Server{ + FrameworkServer: fwserver.Server{ + Provider: &testprovider.Provider{ + ResourcesMethod: func(_ context.Context) []func() resource.Resource { + return []func() resource.Resource{ + func() resource.Resource { + return &testprovider.ResourceWithIdentityAndMoveState{ + Resource: &testprovider.Resource{ + SchemaMethod: func(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = testSchema + }, + MetadataMethod: func(_ context.Context, _ resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = "test_resource" + }, + }, + MoveStateMethod: func(ctx context.Context) []resource.StateMover { + return []resource.StateMover{ + { + StateMover: func(_ context.Context, req resource.MoveStateRequest, resp *resource.MoveStateResponse) { + resp.Diagnostics.Append(resp.TargetIdentity.SetAttribute(ctx, path.Root("test_id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("id"), "test-id-value")...) + resp.Diagnostics.Append(resp.TargetState.SetAttribute(ctx, path.Root("required_attribute"), "true")...) + }, + }, + } + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + } + }, + } + }, + }, + }, + }, + request: &tfprotov5.MoveResourceStateRequest{ + SourceState: testNewTfprotov5RawState(t, map[string]interface{}{ + "id": "test-id-value", + "required_attribute": true, + }), + SourceIdentity: testNewTfprotov5RawState(t, map[string]interface{}{ + "test_id": "test-id-value", + }), + TargetTypeName: "test_resource", + }, + expectedResponse: &tfprotov5.MoveResourceStateResponse{ + TargetState: testNewDynamicValue(t, schemaType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "test-id-value"), + "optional_attribute": tftypes.NewValue(tftypes.String, nil), + "required_attribute": tftypes.NewValue(tftypes.String, "true"), + }), + TargetIdentity: &tfprotov5.ResourceIdentityData{IdentityData: testNewDynamicValue(t, testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "test-id-value"), + })}, + }, + }, } for name, testCase := range testCases { diff --git a/internal/proto6server/server_moveresourcestate.go b/internal/proto6server/server_moveresourcestate.go index 1010b5d7..96e3a96c 100644 --- a/internal/proto6server/server_moveresourcestate.go +++ b/internal/proto6server/server_moveresourcestate.go @@ -5,7 +5,6 @@ package proto6server import ( "context" - "github.com/hashicorp/terraform-plugin-framework/internal/fromproto6" "github.com/hashicorp/terraform-plugin-framework/internal/fwserver" "github.com/hashicorp/terraform-plugin-framework/internal/logging" @@ -36,11 +35,19 @@ func (s *Server) MoveResourceState(ctx context.Context, proto6Req *tfprotov6.Mov fwResp.Diagnostics.Append(diags...) + identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto6Req.TargetTypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.MoveResourceStateResponse(ctx, fwResp), nil + } + if fwResp.Diagnostics.HasError() { return toproto6.MoveResourceStateResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.MoveResourceStateRequest(ctx, proto6Req, resource, resourceSchema) + fwReq, diags := fromproto6.MoveResourceStateRequest(ctx, proto6Req, resource, resourceSchema, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/testing/testprovider/resourcewithidentityandmovestate.go b/internal/testing/testprovider/resourcewithidentityandmovestate.go new file mode 100644 index 00000000..60f4d067 --- /dev/null +++ b/internal/testing/testprovider/resourcewithidentityandmovestate.go @@ -0,0 +1,43 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package testprovider + +import ( + "context" + + "github.com/hashicorp/terraform-plugin-framework/resource" +) + +var _ resource.Resource = &ResourceWithIdentityAndMoveState{} +var _ resource.ResourceWithIdentity = &ResourceWithIdentityAndMoveState{} +var _ resource.ResourceWithMoveState = &ResourceWithIdentityAndMoveState{} + +// Declarative resource.ResourceWithIdentityAndMoveState for unit testing. +type ResourceWithIdentityAndMoveState struct { + *Resource + + // ResourceWithIdentity interface methods + IdentitySchemaMethod func(context.Context, resource.IdentitySchemaRequest, *resource.IdentitySchemaResponse) + + // ResourceWithMoveState interface methods + MoveStateMethod func(context.Context) []resource.StateMover +} + +// IdentitySchema implements resource.ResourceWithIdentity. +func (p *ResourceWithIdentityAndMoveState) IdentitySchema(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + if p.IdentitySchemaMethod == nil { + return + } + + p.IdentitySchemaMethod(ctx, req, resp) +} + +// MoveState satisfies the resource.ResourceWithMoveState interface. +func (r *ResourceWithIdentityAndMoveState) MoveState(ctx context.Context) []resource.StateMover { + if r.MoveStateMethod == nil { + return nil + } + + return r.MoveStateMethod(ctx) +} diff --git a/internal/toproto5/moveresourcestate.go b/internal/toproto5/moveresourcestate.go index bfffadf1..2c598c46 100644 --- a/internal/toproto5/moveresourcestate.go +++ b/internal/toproto5/moveresourcestate.go @@ -27,6 +27,11 @@ func MoveResourceStateResponse(ctx context.Context, fw *fwserver.MoveResourceSta proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) proto5.TargetPrivate = targetPrivate + targetIdentity, diags := ResourceIdentity(ctx, fw.TargetIdentity) + + proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) + proto5.TargetIdentity = targetIdentity + targetState, diags := State(ctx, fw.TargetState) proto5.Diagnostics = append(proto5.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto5/moveresourcestate_test.go b/internal/toproto5/moveresourcestate_test.go index 36075c0b..948ec615 100644 --- a/internal/toproto5/moveresourcestate_test.go +++ b/internal/toproto5/moveresourcestate_test.go @@ -5,6 +5,7 @@ package toproto5_test import ( "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" "testing" "github.com/google/go-cmp/cmp" @@ -33,6 +34,44 @@ func TestMoveResourceStateResponse(t *testing.T) { }, ) + testIdentityProto5Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) + } + + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto5Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testCases := map[string]struct { input *fwserver.MoveResourceStateResponse expected *tfprotov5.MoveResourceStateResponse @@ -152,6 +191,47 @@ func TestMoveResourceStateResponse(t *testing.T) { }, }, }, + "TargetIdentity": { + input: &fwserver.MoveResourceStateResponse{ + TargetIdentity: testIdentity, + }, + expected: &tfprotov5.MoveResourceStateResponse{ + TargetIdentity: &tfprotov5.ResourceIdentityData{ + IdentityData: &testIdentityProto5DynamicValue, + }, + }, + }, + "TargetIdentity-invalid": { + input: &fwserver.MoveResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + TargetIdentity: testIdentityInvalid, + }, + expected: &tfprotov5.MoveResourceStateResponse{ + Diagnostics: []*tfprotov5.Diagnostic{ + { + Severity: tfprotov5.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov5.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/internal/toproto6/moveresourcestate.go b/internal/toproto6/moveresourcestate.go index 86c2a55a..83a81d44 100644 --- a/internal/toproto6/moveresourcestate.go +++ b/internal/toproto6/moveresourcestate.go @@ -27,6 +27,11 @@ func MoveResourceStateResponse(ctx context.Context, fw *fwserver.MoveResourceSta proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) proto6.TargetPrivate = targetPrivate + targetIdentity, diags := ResourceIdentity(ctx, fw.TargetIdentity) + + proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) + proto6.TargetIdentity = targetIdentity + targetState, diags := State(ctx, fw.TargetState) proto6.Diagnostics = append(proto6.Diagnostics, Diagnostics(ctx, diags)...) diff --git a/internal/toproto6/moveresourcestate_test.go b/internal/toproto6/moveresourcestate_test.go index d5281c97..dfed88c6 100644 --- a/internal/toproto6/moveresourcestate_test.go +++ b/internal/toproto6/moveresourcestate_test.go @@ -5,10 +5,11 @@ package toproto6_test import ( "context" + "github.com/hashicorp/terraform-plugin-framework/resource/identityschema" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" "testing" "github.com/google/go-cmp/cmp" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" "github.com/hashicorp/terraform-plugin-go/tftypes" "github.com/hashicorp/terraform-plugin-framework/diag" @@ -33,6 +34,44 @@ func TestMoveResourceStateResponse(t *testing.T) { }, ) + testIdentityProto6Type := tftypes.Object{ + AttributeTypes: map[string]tftypes.Type{ + "test_id": tftypes.String, + }, + } + + testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }) + + testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value) + + if err != nil { + t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) + } + + testIdentity := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.StringAttribute{ + RequiredForImport: true, + }, + }, + }, + } + + testIdentityInvalid := &tfsdk.ResourceIdentity{ + Raw: testIdentityProto6Value, + Schema: identityschema.Schema{ + Attributes: map[string]identityschema.Attribute{ + "test_id": identityschema.BoolAttribute{ + RequiredForImport: true, + }, + }, + }, + } + testCases := map[string]struct { input *fwserver.MoveResourceStateResponse expected *tfprotov6.MoveResourceStateResponse @@ -152,6 +191,47 @@ func TestMoveResourceStateResponse(t *testing.T) { }, }, }, + "TargetIdentity": { + input: &fwserver.MoveResourceStateResponse{ + TargetIdentity: testIdentity, + }, + expected: &tfprotov6.MoveResourceStateResponse{ + TargetIdentity: &tfprotov6.ResourceIdentityData{ + IdentityData: &testIdentityProto6DynamicValue, + }, + }, + }, + "TargetIdentity-invalid": { + input: &fwserver.MoveResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewWarningDiagnostic("test warning summary", "test warning details"), + diag.NewErrorDiagnostic("test error summary", "test error details"), + }, + TargetIdentity: testIdentityInvalid, + }, + expected: &tfprotov6.MoveResourceStateResponse{ + Diagnostics: []*tfprotov6.Diagnostic{ + { + Severity: tfprotov6.DiagnosticSeverityWarning, + Summary: "test warning summary", + Detail: "test warning details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "test error summary", + Detail: "test error details", + }, + { + Severity: tfprotov6.DiagnosticSeverityError, + Summary: "Unable to Convert Resource Identity", + Detail: "An unexpected error was encountered when converting the resource identity to the protocol type. " + + "This is always an issue in terraform-plugin-framework used to implement the provider and should be reported to the provider developers.\n\n" + + "Please report this to the provider developer:\n\n" + + "Unable to create DynamicValue: AttributeName(\"test_id\"): unexpected value type string, tftypes.Bool values must be of type bool", + }, + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/move_state.go b/resource/move_state.go index 6918f5b7..977f565d 100644 --- a/resource/move_state.go +++ b/resource/move_state.go @@ -75,6 +75,14 @@ type MoveStateRequest struct { // other request fields, to determine whether the request should be handled // by this particular implementation. SourceTypeName string + + // SourceIdentity is the identity of the source resource. + // + // Only the underlying JSON field is populated. + SourceIdentity *tfprotov6.RawState + + // SourceIdentitySchemaVersion is the version of the source resource identity. + SourceIdentitySchemaVersion int64 } // MoveStateResponse represents a response to a MoveStateRequest. @@ -107,4 +115,7 @@ type MoveStateResponse struct { // operation. This field is not pre-populated as it is up to implementations // whether the source private data is relevant for the target resource. TargetPrivate *privatestate.ProviderData + + // TargetIdentity is the identity of the target resource. + TargetIdentity *tfsdk.ResourceIdentity } diff --git a/tfsdk/resource_identity.go b/tfsdk/resource_identity.go index d02f1e8e..fd9f3057 100644 --- a/tfsdk/resource_identity.go +++ b/tfsdk/resource_identity.go @@ -5,7 +5,6 @@ package tfsdk import ( "context" - "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/internal/fwschema" "github.com/hashicorp/terraform-plugin-framework/internal/fwschemadata" @@ -70,6 +69,16 @@ func (s *ResourceIdentity) Set(ctx context.Context, val interface{}) diag.Diagno // // Lists can only have the next element added according to the current length. func (s *ResourceIdentity) SetAttribute(ctx context.Context, path path.Path, val interface{}) diag.Diagnostics { + // If s is nil, then calling s.data triggers a nil pointer error so we return the error diag here + if s == nil { + return diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Missing Identity Definition", + "An unexpected error was encountered when attempting to set a resource identity attribute. The resource does not indicate support via a resource identity schema.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer."), + } + } + data := s.data() diags := data.SetAtPath(ctx, path, val)