diff --git a/internal/fromproto5/applyresourcechange.go b/internal/fromproto5/applyresourcechange.go index c32b34387..aa8a06d4e 100644 --- a/internal/fromproto5/applyresourcechange.go +++ b/internal/fromproto5/applyresourcechange.go @@ -17,7 +17,7 @@ import ( // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest // equivalent of a *tfprotov5.ApplyResourceChangeRequest. -func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { +func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -39,9 +39,10 @@ func ApplyResourceChangeRequest(ctx context.Context, proto5 *tfprotov5.ApplyReso } fw := &fwserver.ApplyResourceChangeRequest{ - ResourceSchema: resourceSchema, - IdentitySchema: identitySchema, - Resource: resource, + ResourceSchema: resourceSchema, + ResourceBehavior: resourceBehavior, + IdentitySchema: identitySchema, + Resource: resource, } config, configDiags := Config(ctx, proto5.Config, resourceSchema) diff --git a/internal/fromproto5/applyresourcechange_test.go b/internal/fromproto5/applyresourcechange_test.go index a6b09e011..df272ae6a 100644 --- a/internal/fromproto5/applyresourcechange_test.go +++ b/internal/fromproto5/applyresourcechange_test.go @@ -83,6 +83,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.ApplyResourceChangeRequest + resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema @@ -309,13 +310,26 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "resource-behavior": { + input: &tfprotov5.ApplyResourceChangeRequest{}, + resourceSchema: testFwSchema, + resourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) + got, diags := fromproto5.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto5/readresource.go b/internal/fromproto5/readresource.go index 003424579..8158cc525 100644 --- a/internal/fromproto5/readresource.go +++ b/internal/fromproto5/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov5.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto5 == nil { return nil, nil } @@ -26,6 +26,7 @@ func ReadResourceRequest(ctx context.Context, proto5 *tfprotov5.ReadResourceRequ fw := &fwserver.ReadResourceRequest{ Resource: reqResource, + ResourceBehavior: resourceBehavior, IdentitySchema: identitySchema, ClientCapabilities: ReadResourceClientCapabilities(proto5.ClientCapabilities), } diff --git a/internal/fromproto5/readresource_test.go b/internal/fromproto5/readresource_test.go index 1c8adb7f6..1c175058b 100644 --- a/internal/fromproto5/readresource_test.go +++ b/internal/fromproto5/readresource_test.go @@ -83,6 +83,7 @@ func TestReadResourceRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov5.ReadResourceRequest + resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema identitySchema fwschema.Schema resource resource.Resource @@ -251,13 +252,25 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "resource-behavior": { + input: &tfprotov5.ReadResourceRequest{}, + resourceSchema: testFwSchema, + resourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + expected: &fwserver.ReadResourceRequest{ + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + }, + }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto5.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) + got, diags := fromproto5.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/applyresourcechange.go b/internal/fromproto6/applyresourcechange.go index 5620ffe40..482c4981d 100644 --- a/internal/fromproto6/applyresourcechange.go +++ b/internal/fromproto6/applyresourcechange.go @@ -17,7 +17,7 @@ import ( // ApplyResourceChangeRequest returns the *fwserver.ApplyResourceChangeRequest // equivalent of a *tfprotov6.ApplyResourceChangeRequest. -func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { +func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyResourceChangeRequest, resource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.ApplyResourceChangeRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -39,9 +39,10 @@ func ApplyResourceChangeRequest(ctx context.Context, proto6 *tfprotov6.ApplyReso } fw := &fwserver.ApplyResourceChangeRequest{ - ResourceSchema: resourceSchema, - IdentitySchema: identitySchema, - Resource: resource, + ResourceSchema: resourceSchema, + ResourceBehavior: resourceBehavior, + IdentitySchema: identitySchema, + Resource: resource, } config, configDiags := Config(ctx, proto6.Config, resourceSchema) diff --git a/internal/fromproto6/applyresourcechange_test.go b/internal/fromproto6/applyresourcechange_test.go index d29913fb3..e761e8219 100644 --- a/internal/fromproto6/applyresourcechange_test.go +++ b/internal/fromproto6/applyresourcechange_test.go @@ -83,6 +83,7 @@ func TestApplyResourceChangeRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.ApplyResourceChangeRequest + resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema resource resource.Resource providerMetaSchema fwschema.Schema @@ -309,13 +310,26 @@ func TestApplyResourceChangeRequest(t *testing.T) { ResourceSchema: testFwSchema, }, }, + "resource-behavior": { + input: &tfprotov6.ApplyResourceChangeRequest{}, + resourceSchema: testFwSchema, + resourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + expected: &fwserver.ApplyResourceChangeRequest{ + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + ResourceSchema: testFwSchema, + }, + }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) + got, diags := fromproto6.ApplyResourceChangeRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fromproto6/readresource.go b/internal/fromproto6/readresource.go index 11ea6d844..a7e433773 100644 --- a/internal/fromproto6/readresource.go +++ b/internal/fromproto6/readresource.go @@ -17,7 +17,7 @@ import ( // ReadResourceRequest returns the *fwserver.ReadResourceRequest // equivalent of a *tfprotov6.ReadResourceRequest. -func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { +func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, providerMetaSchema fwschema.Schema, resourceBehavior resource.ResourceBehavior, identitySchema fwschema.Schema) (*fwserver.ReadResourceRequest, diag.Diagnostics) { if proto6 == nil { return nil, nil } @@ -26,6 +26,7 @@ func ReadResourceRequest(ctx context.Context, proto6 *tfprotov6.ReadResourceRequ fw := &fwserver.ReadResourceRequest{ Resource: reqResource, + ResourceBehavior: resourceBehavior, IdentitySchema: identitySchema, ClientCapabilities: ReadResourceClientCapabilities(proto6.ClientCapabilities), } diff --git a/internal/fromproto6/readresource_test.go b/internal/fromproto6/readresource_test.go index e24e9f5bc..facf89b6e 100644 --- a/internal/fromproto6/readresource_test.go +++ b/internal/fromproto6/readresource_test.go @@ -83,6 +83,7 @@ func TestReadResourceRequest(t *testing.T) { testCases := map[string]struct { input *tfprotov6.ReadResourceRequest + resourceBehavior resource.ResourceBehavior resourceSchema fwschema.Schema identitySchema fwschema.Schema resource resource.Resource @@ -251,13 +252,25 @@ func TestReadResourceRequest(t *testing.T) { }, }, }, + "resource-behavior": { + input: &tfprotov6.ReadResourceRequest{}, + resourceSchema: testFwSchema, + resourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + expected: &fwserver.ReadResourceRequest{ + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + }, + }, } for name, testCase := range testCases { t.Run(name, func(t *testing.T) { t.Parallel() - got, diags := fromproto6.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.identitySchema) + got, diags := fromproto6.ReadResourceRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.providerMetaSchema, testCase.resourceBehavior, testCase.identitySchema) if diff := cmp.Diff(got, testCase.expected, cmp.AllowUnexported(privatestate.ProviderData{})); diff != "" { t.Errorf("unexpected difference: %s", diff) diff --git a/internal/fwserver/server_applyresourcechange.go b/internal/fwserver/server_applyresourcechange.go index aabbeeba7..512420c93 100644 --- a/internal/fwserver/server_applyresourcechange.go +++ b/internal/fwserver/server_applyresourcechange.go @@ -17,15 +17,16 @@ import ( // ApplyResourceChangeRequest is the framework server request for the // ApplyResourceChange RPC. type ApplyResourceChangeRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PlannedIdentity *tfsdk.ResourceIdentity - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - IdentitySchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource + ResourceBehavior resource.ResourceBehavior } // ApplyResourceChangeResponse is the framework server response for the @@ -101,15 +102,16 @@ func (s *Server) ApplyResourceChange(ctx context.Context, req *ApplyResourceChan logging.FrameworkTrace(ctx, "ApplyResourceChange running UpdateResource") updateReq := &UpdateResourceRequest{ - Config: req.Config, - PlannedPrivate: req.PlannedPrivate, - PlannedState: req.PlannedState, - PlannedIdentity: req.PlannedIdentity, - PriorState: req.PriorState, - ProviderMeta: req.ProviderMeta, - ResourceSchema: req.ResourceSchema, - IdentitySchema: req.IdentitySchema, - Resource: req.Resource, + Config: req.Config, + PlannedPrivate: req.PlannedPrivate, + PlannedState: req.PlannedState, + PlannedIdentity: req.PlannedIdentity, + PriorState: req.PriorState, + ProviderMeta: req.ProviderMeta, + ResourceSchema: req.ResourceSchema, + IdentitySchema: req.IdentitySchema, + Resource: req.Resource, + ResourceBehavior: req.ResourceBehavior, } updateResp := &UpdateResourceResponse{} diff --git a/internal/fwserver/server_applyresourcechange_test.go b/internal/fwserver/server_applyresourcechange_test.go index 0fa2d9ab9..36ade5894 100644 --- a/internal/fwserver/server_applyresourcechange_test.go +++ b/internal/fwserver/server_applyresourcechange_test.go @@ -524,6 +524,73 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, + "create-response-newidentity-changes": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + PriorState: testEmptyState, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + + // Prevent missing resource state error diagnostic + var data testSchemaData + + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Delete") + }, + UpdateMethod: func(_ context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Create, Got: Update") + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, "create-response-newstate-null": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -1591,7 +1658,211 @@ func TestServerApplyResourceChange(t *testing.T) { Private: testEmptyPrivate, }, }, - "update-response-newidentity": { + "update-response-newidentity-null-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "update-response-newidentity-with-plannedidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("id-123"), + })...) + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "update-invalid-response-newidentity-changes": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ApplyResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Create") + }, + DeleteMethod: func(_ context.Context, _ resource.DeleteRequest, resp *resource.DeleteResponse) { + resp.Diagnostics.AddError("Unexpected Method Call", "Expected: Update, Got: Delete") + }, + UpdateMethod: func(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + }, + }, + }, + expectedResponse: &fwserver.ApplyResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Identity Change", + "During the update operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + "Planned Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"id-123\">>\n\n"+ + "New Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"new-id-123\">>", + ), + }, + NewIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + NewState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + Private: testEmptyPrivate, + }, + }, + "update-valid-response-mutable-identity-newidentity-changes": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -1610,6 +1881,12 @@ func TestServerApplyResourceChange(t *testing.T) { }), Schema: testSchema, }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, PriorState: &tfsdk.State{ Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ "test_computed": tftypes.NewValue(tftypes.String, nil), @@ -1619,6 +1896,9 @@ func TestServerApplyResourceChange(t *testing.T) { }, IdentitySchema: testIdentitySchema, ResourceSchema: testSchema, + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, Resource: &testprovider.ResourceWithIdentity{ Resource: &testprovider.Resource{ CreateMethod: func(_ context.Context, _ resource.CreateRequest, resp *resource.CreateResponse) { diff --git a/internal/fwserver/server_importresourcestate.go b/internal/fwserver/server_importresourcestate.go index 000a0fad6..f2201f971 100644 --- a/internal/fwserver/server_importresourcestate.go +++ b/internal/fwserver/server_importresourcestate.go @@ -205,6 +205,12 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta private := &privatestate.Data{} + // Set an internal private field that will get sent alongside the imported resource. This will be cleared by + // the following ReadResource RPC and is primarily used to control validation of resource identities during refresh. + private.Framework = map[string][]byte{ + privatestate.ImportBeforeReadKey: []byte(`true`), // The actual data isn't important, we just use the map key to detect it. + } + if importResp.Private != nil { private.Provider = importResp.Private } diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index ea9b1ac68..9753fe346 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -166,12 +166,18 @@ func TestServerImportResourceState(t *testing.T) { testProviderData := privatestate.MustProviderData(context.Background(), testProviderKeyValue) testPrivate := &privatestate.Data{ + Framework: map[string][]byte{ + privatestate.ImportBeforeReadKey: []byte(`true`), + }, Provider: testProviderData, } testEmptyProviderData := privatestate.EmptyProviderData(context.Background()) testEmptyPrivate := &privatestate.Data{ + Framework: map[string][]byte{ + privatestate.ImportBeforeReadKey: []byte(`true`), + }, Provider: testEmptyProviderData, } diff --git a/internal/fwserver/server_planresourcechange.go b/internal/fwserver/server_planresourcechange.go index fb6c161d8..1668c0819 100644 --- a/internal/fwserver/server_planresourcechange.go +++ b/internal/fwserver/server_planresourcechange.go @@ -325,21 +325,24 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange modifyPlanReq.ProviderMeta = *req.ProviderMeta } - if resp.PlannedIdentity != nil { - modifyPlanReq.Identity = &tfsdk.ResourceIdentity{ - Schema: resp.PlannedIdentity.Schema, - Raw: resp.PlannedIdentity.Raw.Copy(), - } - } - modifyPlanResp := resource.ModifyPlanResponse{ Diagnostics: resp.Diagnostics, Plan: modifyPlanReq.Plan, - Identity: modifyPlanReq.Identity, RequiresReplace: path.Paths{}, Private: modifyPlanReq.Private, } + if resp.PlannedIdentity != nil { + modifyPlanReq.Identity = &tfsdk.ResourceIdentity{ + Schema: resp.PlannedIdentity.Schema, + Raw: resp.PlannedIdentity.Raw.Copy(), + } + modifyPlanResp.Identity = &tfsdk.ResourceIdentity{ + Schema: resp.PlannedIdentity.Schema, + Raw: resp.PlannedIdentity.Raw.Copy(), + } + } + logging.FrameworkTrace(ctx, "Calling provider defined Resource ModifyPlan") resourceWithModifyPlan.ModifyPlan(ctx, modifyPlanReq, &modifyPlanResp) logging.FrameworkTrace(ctx, "Called provider defined Resource ModifyPlan") @@ -368,14 +371,29 @@ func (s *Server) PlanResourceChange(ctx context.Context, req *PlanResourceChange } } - if resp.PlannedIdentity != nil && req.IdentitySchema == nil { - resp.Diagnostics.AddError( - "Unexpected Plan Response", - "An unexpected error was encountered when creating the plan response. New identity data was returned by the provider planning 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.", - ) + if resp.PlannedIdentity != nil { + if req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Plan Response", + "An unexpected error was encountered when creating the plan response. New identity data was returned by the provider planning 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 + return + } + + // If we're updating or deleting and we already have an identity stored, validate that the planned identity isn't changing + if !req.ResourceBehavior.MutableIdentity && !req.PriorState.Raw.IsNull() && !req.PriorIdentity.Raw.IsNull() && !req.PriorIdentity.Raw.Equal(resp.PlannedIdentity.Raw) { + resp.Diagnostics.AddError( + "Unexpected Identity Change", + "During the planning operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + fmt.Sprintf("Prior Identity: %s\n\n", req.PriorIdentity.Raw.String())+ + fmt.Sprintf("Planned Identity: %s", resp.PlannedIdentity.Raw.String()), + ) + + return + } } // Ensure deterministic RequiresReplace by sorting and deduplicating diff --git a/internal/fwserver/server_planresourcechange_test.go b/internal/fwserver/server_planresourcechange_test.go index e8cbf3ad7..460a4f8c9 100644 --- a/internal/fwserver/server_planresourcechange_test.go +++ b/internal/fwserver/server_planresourcechange_test.go @@ -3518,8 +3518,7 @@ func TestServerPlanResourceChange(t *testing.T) { }), Schema: testSchema, }, - PriorState: testEmptyState, - // Resource supports identity but there isn't one in state yet + PriorState: testEmptyState, PriorIdentity: nil, IdentitySchema: testIdentitySchema, ResourceSchema: testSchema, @@ -3534,66 +3533,7 @@ func TestServerPlanResourceChange(t *testing.T) { TestID: types.StringValue("new-id-123"), } - resp.Diagnostics.Append(req.Identity.Set(ctx, &data)...) - }, - IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { - resp.IdentitySchema = testIdentitySchema - }, - }, - }, - expectedResponse: &fwserver.PlanResourceChangeResponse{ - PlannedState: &tfsdk.State{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, tftypes.UnknownValue), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PlannedIdentity: &tfsdk.ResourceIdentity{ - Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ - "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), - }), - Schema: testIdentitySchema, - }, - PlannedPrivate: testEmptyPrivate, - }, - }, - "create-resourcewithmodifyplan-response-plannedidentity-update": { - server: &fwserver.Server{ - Provider: &testprovider.Provider{}, - }, - request: &fwserver.PlanResourceChangeRequest{ - Config: &tfsdk.Config{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - ProposedNewState: &tfsdk.Plan{ - Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ - "test_computed": tftypes.NewValue(tftypes.String, nil), - "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), - }), - Schema: testSchema, - }, - PriorState: testEmptyState, - PriorIdentity: &tfsdk.ResourceIdentity{ - Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ - "test_id": tftypes.NewValue(tftypes.String, "id-123"), - }), - Schema: testIdentitySchema, - }, - IdentitySchema: testIdentitySchema, - ResourceSchema: testSchema, - Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ - ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { - var data testIdentitySchemaData - resp.Diagnostics.Append(req.Identity.Get(ctx, &data)...) - - data.TestID = types.StringValue("new-id-123") - - resp.Diagnostics.Append(req.Identity.Set(ctx, &data)...) + resp.Diagnostics.Append(resp.Identity.Set(ctx, &data)...) }, IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { resp.IdentitySchema = testIdentitySchema @@ -4051,6 +3991,157 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "delete-resourcewithmodifyplan-no-prioridentity-plannedidentity-changed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-state-value"), + }), + Schema: testSchema, + }, + // It's possible for an identity to not be in state while updating (like a refresh=false plan) + PriorIdentity: nil, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedState: testEmptyState, + PlannedPrivate: testEmptyPrivate, + }, + }, + "delete-resourcewithmodifyplan-invalid-response-plannedidentity-changed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-state-value"), + }), + Schema: testSchema, + }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var identityData testIdentitySchemaData + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + identityData.TestID = types.StringValue("new-id-123") + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Identity Change", + "During the planning operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + "Prior Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"id-123\">>\n\n"+ + "Planned Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"new-id-123\">>", + ), + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedState: testEmptyState, + PlannedPrivate: testEmptyPrivate, + }, + }, + "delete-resourcewithmodifyplan-mutable-identity-response-plannedidentity-changed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-config-value"), + }), + Schema: testSchema, + }, + ProposedNewState: testEmptyPlan, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-state-value"), + }), + Schema: testSchema, + }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var identityData testIdentitySchemaData + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + identityData.TestID = types.StringValue("new-id-123") + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedState: testEmptyState, + PlannedPrivate: testEmptyPrivate, + }, + }, "delete-resourcewithmodifyplan-response-requiresreplace": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, @@ -6568,6 +6659,217 @@ func TestServerPlanResourceChange(t *testing.T) { PlannedPrivate: testEmptyPrivate, }, }, + "update-resourcewithmodifyplan-no-prioridentity-plannedidentity-changed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + // It's possible for an identity to not be in state while updating (like a refresh=false plan) + PriorIdentity: nil, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + data.TestComputed = types.StringValue("test-plannedstate-value") + resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...) + + resp.Diagnostics.Append(resp.Identity.Set(ctx, testIdentitySchemaData{ + TestID: types.StringValue("new-id-123"), + })...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "update-resourcewithmodifyplan-invalid-response-plannedidentity-changed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + data.TestComputed = types.StringValue("test-plannedstate-value") + resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...) + + var identityData testIdentitySchemaData + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + identityData.TestID = types.StringValue("new-id-123") + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Identity Change", + "During the planning operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + "Prior Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"id-123\">>\n\n"+ + "Planned Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"new-id-123\">>", + ), + }, + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, + "update-resourcewithmodifyplan-mutable-identity-response-plannedidentity-changed": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.PlanResourceChangeRequest{ + Config: &tfsdk.Config{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + ProposedNewState: &tfsdk.Plan{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PriorState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, nil), + "test_required": tftypes.NewValue(tftypes.String, "test-old-value"), + }), + Schema: testSchema, + }, + PriorIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "id-123"), + }), + Schema: testIdentitySchema, + }, + IdentitySchema: testIdentitySchema, + ResourceSchema: testSchema, + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + Resource: &testprovider.ResourceWithIdentityAndModifyPlan{ + ModifyPlanMethod: func(ctx context.Context, req resource.ModifyPlanRequest, resp *resource.ModifyPlanResponse) { + var data testSchemaData + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + data.TestComputed = types.StringValue("test-plannedstate-value") + resp.Diagnostics.Append(resp.Plan.Set(ctx, &data)...) + + var identityData testIdentitySchemaData + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + identityData.TestID = types.StringValue("new-id-123") + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + IdentitySchemaMethod: func(ctx context.Context, req resource.IdentitySchemaRequest, resp *resource.IdentitySchemaResponse) { + resp.IdentitySchema = testIdentitySchema + }, + }, + }, + expectedResponse: &fwserver.PlanResourceChangeResponse{ + PlannedIdentity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentitySchemaType, map[string]tftypes.Value{ + "test_id": tftypes.NewValue(tftypes.String, "new-id-123"), + }), + Schema: testIdentitySchema, + }, + PlannedState: &tfsdk.State{ + Raw: tftypes.NewValue(testSchemaType, map[string]tftypes.Value{ + "test_computed": tftypes.NewValue(tftypes.String, "test-plannedstate-value"), + "test_required": tftypes.NewValue(tftypes.String, "test-new-value"), + }), + Schema: testSchema, + }, + PlannedPrivate: testEmptyPrivate, + }, + }, "update-resourcewithmodifyplan-response-requiresreplace": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, diff --git a/internal/fwserver/server_readresource.go b/internal/fwserver/server_readresource.go index 557b1a714..eaffd4ff0 100644 --- a/internal/fwserver/server_readresource.go +++ b/internal/fwserver/server_readresource.go @@ -5,6 +5,7 @@ package fwserver import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -27,6 +28,7 @@ type ReadResourceRequest struct { Resource resource.Resource Private *privatestate.Data ProviderMeta *tfsdk.Config + ResourceBehavior resource.ResourceBehavior } // ReadResourceResponse is the framework server response for the @@ -110,12 +112,21 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res readReq.Private = privateProviderData readResp.Private = privateProviderData + readFollowingImport := false if req.Private != nil { if req.Private.Provider != nil { readReq.Private = req.Private.Provider readResp.Private = req.Private.Provider } + // This internal private field is set on a resource during ImportResourceState to help framework determine if + // the resource has been recently imported. We only need to read this once, so we immediately clear it after. + _, ok := req.Private.Framework[privatestate.ImportBeforeReadKey] + if ok { + readFollowingImport = true + delete(req.Private.Framework, privatestate.ImportBeforeReadKey) + } + resp.Private = req.Private } @@ -162,14 +173,29 @@ func (s *Server) ReadResource(ctx context.Context, req *ReadResourceRequest, res return } - if resp.NewIdentity != nil && req.IdentitySchema == nil { - resp.Diagnostics.AddError( - "Unexpected Read Response", - "An unexpected error was encountered when creating the read response. New identity data was returned by the provider read 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.", - ) + if resp.NewIdentity != nil { + if req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Read Response", + "An unexpected error was encountered when creating the read response. New identity data was returned by the provider read 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 + return + } + + // If we're refreshing the resource state (excluding a recently imported resource), validate that the new identity isn't changing + if !req.ResourceBehavior.MutableIdentity && !readFollowingImport && !req.CurrentIdentity.Raw.IsNull() && !req.CurrentIdentity.Raw.Equal(resp.NewIdentity.Raw) { + resp.Diagnostics.AddError( + "Unexpected Identity Change", + "During the read operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + fmt.Sprintf("Current Identity: %s\n\n", req.CurrentIdentity.Raw.String())+ + fmt.Sprintf("New Identity: %s", resp.NewIdentity.Raw.String()), + ) + + return + } } semanticEqualityReq := SchemaSemanticEqualityRequest{ diff --git a/internal/fwserver/server_readresource_test.go b/internal/fwserver/server_readresource_test.go index a846d389c..d4dfe250c 100644 --- a/internal/fwserver/server_readresource_test.go +++ b/internal/fwserver/server_readresource_test.go @@ -198,6 +198,13 @@ func TestServerReadResource(t *testing.T) { Provider: testEmptyProviderData, } + testPrivateAfterImport := &privatestate.Data{ + Framework: map[string][]byte{ + privatestate.ImportBeforeReadKey: []byte(`true`), + }, + Provider: testEmptyProviderData, + } + testDeferralAllowed := resource.ReadClientCapabilities{ DeferralAllowed: true, } @@ -629,7 +636,68 @@ func TestServerReadResource(t *testing.T) { Private: testEmptyPrivate, }, }, - "response-identity-update": { + "response-identity-valid-update-null-currentidentity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + identityData := struct { + TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), + } + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, + "response-identity-valid-update-after-import": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + CurrentIdentity: testCurrentIdentity, + Private: testPrivateAfterImport, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + }, + expectedResponse: &fwserver.ReadResourceResponse{ + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: &privatestate.Data{ + Framework: make(map[string][]byte, 0), // Private import key should be cleared + Provider: testEmptyProviderData, + }, + }, + }, + "response-identity-invalid-update": { server: &fwserver.Server{ Provider: &testprovider.Provider{}, }, @@ -653,6 +721,48 @@ func TestServerReadResource(t *testing.T) { }, }, }, + expectedResponse: &fwserver.ReadResourceResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewErrorDiagnostic( + "Unexpected Identity Change", + "During the read operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + "Current Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"id-123\">>\n\n"+ + "New Identity: tftypes.Object[\"test_id\":tftypes.String]<\"test_id\":tftypes.String<\"new-id-123\">>", + ), + }, + NewState: testCurrentState, + NewIdentity: testNewIdentity, + Private: testEmptyPrivate, + }, + }, + "response-identity-valid-update-mutable-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ReadResourceRequest{ + CurrentState: testCurrentState, + CurrentIdentity: testCurrentIdentity, + ResourceBehavior: resource.ResourceBehavior{ + MutableIdentity: true, + }, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithIdentity{ + Resource: &testprovider.Resource{ + ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var identityData struct { + TestID types.String `tfsdk:"test_id"` + } + + resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) + + identityData.TestID = types.StringValue("new-id-123") + + resp.Diagnostics.Append(resp.Identity.Set(ctx, &identityData)...) + }, + }, + }, + }, expectedResponse: &fwserver.ReadResourceResponse{ NewState: testCurrentState, NewIdentity: testNewIdentity, diff --git a/internal/fwserver/server_updateresource.go b/internal/fwserver/server_updateresource.go index 041dee187..558790b73 100644 --- a/internal/fwserver/server_updateresource.go +++ b/internal/fwserver/server_updateresource.go @@ -5,6 +5,7 @@ package fwserver import ( "context" + "fmt" "github.com/hashicorp/terraform-plugin-go/tftypes" @@ -20,15 +21,16 @@ import ( // UpdateResourceRequest is the framework server request for an update request // with the ApplyResourceChange RPC. type UpdateResourceRequest struct { - Config *tfsdk.Config - PlannedPrivate *privatestate.Data - PlannedState *tfsdk.Plan - PlannedIdentity *tfsdk.ResourceIdentity - PriorState *tfsdk.State - ProviderMeta *tfsdk.Config - ResourceSchema fwschema.Schema - IdentitySchema fwschema.Schema - Resource resource.Resource + Config *tfsdk.Config + PlannedPrivate *privatestate.Data + PlannedState *tfsdk.Plan + PlannedIdentity *tfsdk.ResourceIdentity + PriorState *tfsdk.State + ProviderMeta *tfsdk.Config + ResourceSchema fwschema.Schema + IdentitySchema fwschema.Schema + Resource resource.Resource + ResourceBehavior resource.ResourceBehavior } // UpdateResourceResponse is the framework server response for an update request @@ -172,14 +174,29 @@ func (s *Server) UpdateResource(ctx context.Context, req *UpdateResourceRequest, return } - if resp.NewIdentity != nil && req.IdentitySchema == nil { - resp.Diagnostics.AddError( - "Unexpected Update Response", - "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider update 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.", - ) + if resp.NewIdentity != nil { + if req.IdentitySchema == nil { + resp.Diagnostics.AddError( + "Unexpected Update Response", + "An unexpected error was encountered when creating the apply response. New identity data was returned by the provider update 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 + return + } + + // If we already have an identity stored, validate that the new identity hasn't changing + if !req.ResourceBehavior.MutableIdentity && !req.PlannedIdentity.Raw.IsNull() && !req.PlannedIdentity.Raw.Equal(resp.NewIdentity.Raw) { + resp.Diagnostics.AddError( + "Unexpected Identity Change", + "During the update operation, the Terraform Provider unexpectedly returned a different identity then the previously stored one.\n\n"+ + "This is always a problem with the provider and should be reported to the provider developer.\n\n"+ + fmt.Sprintf("Planned Identity: %s\n\n", req.PlannedIdentity.Raw.String())+ + fmt.Sprintf("New Identity: %s", resp.NewIdentity.Raw.String()), + ) + + return + } } semanticEqualityReq := SchemaSemanticEqualityRequest{ diff --git a/internal/privatestate/data.go b/internal/privatestate/data.go index 5493a9d97..e98610bbe 100644 --- a/internal/privatestate/data.go +++ b/internal/privatestate/data.go @@ -17,6 +17,13 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/logging" ) +// ImportBeforeReadKey is an internal private field used to indicate that the current resource state and identity +// were provided most recently by the ImportResourceState RPC. This indicates that the state is an import stub and identity +// has not been stored in state yet. +// +// When detected, this key should be cleared before returning from the ReadResource RPC. +var ImportBeforeReadKey = ".import_before_read" + // Data contains private state data for the framework and providers. type Data struct { // Potential future usage: diff --git a/internal/proto5server/server_applyresourcechange.go b/internal/proto5server/server_applyresourcechange.go index 717d071f2..7964404a1 100644 --- a/internal/proto5server/server_applyresourcechange.go +++ b/internal/proto5server/server_applyresourcechange.go @@ -52,7 +52,15 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto5Req *tfprotov5.A return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.ApplyResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, identitySchema) + resourceBehavior, diags := s.FrameworkServer.ResourceBehavior(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ApplyResourceChangeResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.ApplyResourceChangeRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_importresourcestate_test.go b/internal/proto5server/server_importresourcestate_test.go index e2b539199..b27fc40d5 100644 --- a/internal/proto5server/server_importresourcestate_test.go +++ b/internal/proto5server/server_importresourcestate_test.go @@ -60,6 +60,10 @@ func TestServerImportResourceState(t *testing.T) { t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err) } + testEmptyPrivateBytes := privatestate.MustMarshalToJson(map[string][]byte{ + privatestate.ImportBeforeReadKey: []byte(`true`), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -130,6 +134,7 @@ func TestServerImportResourceState(t *testing.T) { { State: testStateDynamicValue, TypeName: "test_resource", + Private: testEmptyPrivateBytes, }, }, }, @@ -183,7 +188,8 @@ func TestServerImportResourceState(t *testing.T) { expectedResponse: &tfprotov5.ImportResourceStateResponse{ ImportedResources: []*tfprotov5.ImportedResource{ { - State: &testEmptyStateDynamicValue, + State: &testEmptyStateDynamicValue, + Private: testEmptyPrivateBytes, Identity: &tfprotov5.ResourceIdentityData{ IdentityData: testRequestIdentityValue, }, @@ -271,6 +277,7 @@ func TestServerImportResourceState(t *testing.T) { expectedResponse: &tfprotov5.ImportResourceStateResponse{ ImportedResources: []*tfprotov5.ImportedResource{ { + Private: testEmptyPrivateBytes, State: testStateDynamicValue, TypeName: "test_resource", }, @@ -317,7 +324,8 @@ func TestServerImportResourceState(t *testing.T) { expectedResponse: &tfprotov5.ImportResourceStateResponse{ ImportedResources: []*tfprotov5.ImportedResource{ { - State: &testEmptyStateDynamicValue, + State: &testEmptyStateDynamicValue, + Private: testEmptyPrivateBytes, Identity: &tfprotov5.ResourceIdentityData{ IdentityData: testImportedResourceIdentityDynamicValue, }, @@ -366,7 +374,8 @@ func TestServerImportResourceState(t *testing.T) { State: testStateDynamicValue, TypeName: "test_resource", Private: privatestate.MustMarshalToJson(map[string][]byte{ - "providerKey": []byte(`{"key": "value"}`), + privatestate.ImportBeforeReadKey: []byte(`true`), + "providerKey": []byte(`{"key": "value"}`), }), }, }, diff --git a/internal/proto5server/server_readresource.go b/internal/proto5server/server_readresource.go index 299b67835..9e0e78689 100644 --- a/internal/proto5server/server_readresource.go +++ b/internal/proto5server/server_readresource.go @@ -53,7 +53,15 @@ func (s *Server) ReadResource(ctx context.Context, proto5Req *tfprotov5.ReadReso return toproto5.ReadResourceResponse(ctx, fwResp), nil } - fwReq, diags := fromproto5.ReadResourceRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, identitySchema) + resourceBehavior, diags := s.FrameworkServer.ResourceBehavior(ctx, proto5Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto5.ReadResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto5.ReadResourceRequest(ctx, proto5Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto5server/server_readresource_test.go b/internal/proto5server/server_readresource_test.go index 9617d7b7a..0f3fa63c1 100644 --- a/internal/proto5server/server_readresource_test.go +++ b/internal/proto5server/server_readresource_test.go @@ -432,14 +432,12 @@ func TestServerReadResource(t *testing.T) { resp.TypeName = "test_resource" }, ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var identityData struct { + identityData := struct { TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), } - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) - - identityData.TestID = types.StringValue("new-id-123") - resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) }, }, @@ -455,10 +453,7 @@ func TestServerReadResource(t *testing.T) { }, request: &tfprotov5.ReadResourceRequest{ CurrentState: testEmptyDynamicValue, - CurrentIdentity: &tfprotov5.ResourceIdentityData{ - IdentityData: testCurrentIdentityValue, - }, - TypeName: "test_resource", + TypeName: "test_resource", }, expectedResponse: &tfprotov5.ReadResourceResponse{ NewState: testEmptyDynamicValue, diff --git a/internal/proto6server/server_applyresourcechange.go b/internal/proto6server/server_applyresourcechange.go index 85fc2dc11..5d6e62f82 100644 --- a/internal/proto6server/server_applyresourcechange.go +++ b/internal/proto6server/server_applyresourcechange.go @@ -52,7 +52,15 @@ func (s *Server) ApplyResourceChange(ctx context.Context, proto6Req *tfprotov6.A return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.ApplyResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, identitySchema) + resourceBehavior, diags := s.FrameworkServer.ResourceBehavior(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ApplyResourceChangeResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.ApplyResourceChangeRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_importresourcestate_test.go b/internal/proto6server/server_importresourcestate_test.go index f59f287b4..7cf498cbe 100644 --- a/internal/proto6server/server_importresourcestate_test.go +++ b/internal/proto6server/server_importresourcestate_test.go @@ -60,6 +60,10 @@ func TestServerImportResourceState(t *testing.T) { t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err) } + testEmptyPrivateBytes := privatestate.MustMarshalToJson(map[string][]byte{ + privatestate.ImportBeforeReadKey: []byte(`true`), + }) + testSchema := schema.Schema{ Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ @@ -129,6 +133,7 @@ func TestServerImportResourceState(t *testing.T) { ImportedResources: []*tfprotov6.ImportedResource{ { State: testStateDynamicValue, + Private: testEmptyPrivateBytes, TypeName: "test_resource", }, }, @@ -183,7 +188,8 @@ func TestServerImportResourceState(t *testing.T) { expectedResponse: &tfprotov6.ImportResourceStateResponse{ ImportedResources: []*tfprotov6.ImportedResource{ { - State: &testEmptyStateDynamicValue, + State: &testEmptyStateDynamicValue, + Private: testEmptyPrivateBytes, Identity: &tfprotov6.ResourceIdentityData{ IdentityData: testRequestIdentityValue, }, @@ -271,6 +277,7 @@ func TestServerImportResourceState(t *testing.T) { expectedResponse: &tfprotov6.ImportResourceStateResponse{ ImportedResources: []*tfprotov6.ImportedResource{ { + Private: testEmptyPrivateBytes, State: testStateDynamicValue, TypeName: "test_resource", }, @@ -317,7 +324,8 @@ func TestServerImportResourceState(t *testing.T) { expectedResponse: &tfprotov6.ImportResourceStateResponse{ ImportedResources: []*tfprotov6.ImportedResource{ { - State: &testEmptyStateDynamicValue, + Private: testEmptyPrivateBytes, + State: &testEmptyStateDynamicValue, Identity: &tfprotov6.ResourceIdentityData{ IdentityData: testImportedResourceIdentityDynamicValue, }, @@ -366,7 +374,8 @@ func TestServerImportResourceState(t *testing.T) { State: testStateDynamicValue, TypeName: "test_resource", Private: privatestate.MustMarshalToJson(map[string][]byte{ - "providerKey": []byte(`{"key": "value"}`), + privatestate.ImportBeforeReadKey: []byte(`true`), + "providerKey": []byte(`{"key": "value"}`), }), }, }, diff --git a/internal/proto6server/server_readresource.go b/internal/proto6server/server_readresource.go index 89cec5523..0da8885be 100644 --- a/internal/proto6server/server_readresource.go +++ b/internal/proto6server/server_readresource.go @@ -52,7 +52,15 @@ func (s *Server) ReadResource(ctx context.Context, proto6Req *tfprotov6.ReadReso return toproto6.ReadResourceResponse(ctx, fwResp), nil } - fwReq, diags := fromproto6.ReadResourceRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, identitySchema) + resourceBehavior, diags := s.FrameworkServer.ResourceBehavior(ctx, proto6Req.TypeName) + + fwResp.Diagnostics.Append(diags...) + + if fwResp.Diagnostics.HasError() { + return toproto6.ReadResourceResponse(ctx, fwResp), nil + } + + fwReq, diags := fromproto6.ReadResourceRequest(ctx, proto6Req, resource, resourceSchema, providerMetaSchema, resourceBehavior, identitySchema) fwResp.Diagnostics.Append(diags...) diff --git a/internal/proto6server/server_readresource_test.go b/internal/proto6server/server_readresource_test.go index 2f096bbef..4303d2b36 100644 --- a/internal/proto6server/server_readresource_test.go +++ b/internal/proto6server/server_readresource_test.go @@ -432,14 +432,12 @@ func TestServerReadResource(t *testing.T) { resp.TypeName = "test_resource" }, ReadMethod: func(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { - var identityData struct { + identityData := struct { TestID types.String `tfsdk:"test_id"` + }{ + TestID: types.StringValue("new-id-123"), } - resp.Diagnostics.Append(req.Identity.Get(ctx, &identityData)...) - - identityData.TestID = types.StringValue("new-id-123") - resp.Diagnostics.Append(resp.Identity.Set(ctx, identityData)...) }, }, @@ -455,10 +453,7 @@ func TestServerReadResource(t *testing.T) { }, request: &tfprotov6.ReadResourceRequest{ CurrentState: testEmptyDynamicValue, - CurrentIdentity: &tfprotov6.ResourceIdentityData{ - IdentityData: testCurrentIdentityValue, - }, - TypeName: "test_resource", + TypeName: "test_resource", }, expectedResponse: &tfprotov6.ReadResourceResponse{ NewState: testEmptyDynamicValue, diff --git a/resource/metadata.go b/resource/metadata.go index 289cd6ab2..2977f3057 100644 --- a/resource/metadata.go +++ b/resource/metadata.go @@ -36,6 +36,11 @@ type ResourceBehavior struct { // NOTE: This functionality is related to deferred action support, which is currently experimental and is subject // to change or break without warning. It is not protected by version compatibility guarantees. ProviderDeferred ProviderDeferredBehavior + + // MutableIdentity indicates that the managed resource supports an identity that can change during the + // resource's lifecycle. Setting this flag to true will disable the SDK validation that ensures identity + // data doesn't change during RPC calls. + MutableIdentity bool } // ProviderDeferredBehavior enables provider-defined logic to be executed