From 43b7acf6cad5ec3560759e50259a0170f25f516e Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 24 Apr 2025 07:23:52 -0500 Subject: [PATCH 1/2] Resource Identity: Add a new import helper that can pass-through an identity attribute and an import ID. --- .../server_importresourcestate_test.go | 125 ++++++++++++++++++ resource/import_state.go | 50 ++++++- 2 files changed, 172 insertions(+), 3 deletions(-) diff --git a/internal/fwserver/server_importresourcestate_test.go b/internal/fwserver/server_importresourcestate_test.go index ef9a7ac36..ea9b1ac68 100644 --- a/internal/fwserver/server_importresourcestate_test.go +++ b/internal/fwserver/server_importresourcestate_test.go @@ -145,6 +145,15 @@ func TestServerImportResourceState(t *testing.T) { Schema: testSchema, } + testStatePassThroughIdentity := &tfsdk.State{ + Raw: tftypes.NewValue(testType, map[string]tftypes.Value{ + "id": tftypes.NewValue(tftypes.String, "id-123"), + "optional": tftypes.NewValue(tftypes.String, nil), + "required": tftypes.NewValue(tftypes.String, nil), + }), + Schema: testSchema, + } + testImportedResourceIdentity := &tfsdk.ResourceIdentity{ Raw: testImportedResourceIdentityValue, Schema: testIdentitySchema, @@ -655,6 +664,122 @@ func TestServerImportResourceState(t *testing.T) { }, }, }, + "response-importedresources-passthrough-identity-imported-by-id": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "id-123", + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("id"), path.Root("test_id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testStatePassThroughIdentity, + Identity: &tfsdk.ResourceIdentity{ + Raw: tftypes.NewValue(testIdentityType, nil), + Schema: testIdentitySchema, + }, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-passthrough-identity-imported-by-identity": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + Identity: testRequestIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resp.Diagnostics.Append(resp.Identity.SetAttribute(ctx, path.Root("other_test_id"), types.StringValue("new-value-123"))...) + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("id"), path.Root("test_id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + ImportedResources: []fwserver.ImportedResource{ + { + State: *testStatePassThroughIdentity, + Identity: testImportedResourceIdentity, + TypeName: "test_resource", + Private: testEmptyPrivate, + }, + }, + }, + }, + "response-importedresources-passthrough-identity-invalid-state-path": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + ID: "id-123", + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("not-valid"), path.Root("test_id"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("not-valid"), + "State Write Error", + "An unexpected error was encountered trying to retrieve type information at a given path. "+ + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"not-valid\") still remains in the path: could not find attribute or block "+ + "\"not-valid\" in schema", + ), + }, + }, + }, + "response-importedresources-passthrough-identity-invalid-identity-path": { + server: &fwserver.Server{ + Provider: &testprovider.Provider{}, + }, + request: &fwserver.ImportResourceStateRequest{ + EmptyState: *testEmptyState, + Identity: testRequestIdentity, + IdentitySchema: testIdentitySchema, + Resource: &testprovider.ResourceWithImportState{ + Resource: &testprovider.Resource{}, + ImportStateMethod: func(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughWithIdentity(ctx, path.Root("id"), path.Root("not-valid"), req, resp) + }, + }, + TypeName: "test_resource", + }, + expectedResponse: &fwserver.ImportResourceStateResponse{ + Diagnostics: diag.Diagnostics{ + diag.NewAttributeErrorDiagnostic( + path.Root("not-valid"), + "Resource Identity Read Error", + "An unexpected error was encountered trying to retrieve type information at a given path. "+ + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Error: AttributeName(\"not-valid\") still remains in the path: could not find attribute or block "+ + "\"not-valid\" in schema", + ), + }, + }, + }, } for name, testCase := range testCases { diff --git a/resource/import_state.go b/resource/import_state.go index 1ca670448..ab3635fc1 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -10,6 +10,7 @@ import ( "github.com/hashicorp/terraform-plugin-framework/internal/privatestate" "github.com/hashicorp/terraform-plugin-framework/path" "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" ) // ImportStateClientCapabilities allows Terraform to publish information @@ -95,9 +96,9 @@ type ImportStateResponse struct { // identifier to a given state attribute path. The attribute must accept a // string value. // -// This method will also automatically pass through the Identity field if imported by -// the identity attribute of a import config block (Terraform 1.12+ and later). In this -// scenario where identity is provided instead of the string ID, the state field defined +// For resources that support identity, this method will also automatically pass through the +// Identity field if imported by the identity attribute of a import config block (Terraform 1.12+ and later). +// In this scenario where identity is provided instead of the string ID, the state field defined // at `attrPath` will be set to null. func ImportStatePassthroughID(ctx context.Context, attrPath path.Path, req ImportStateRequest, resp *ImportStateResponse) { if attrPath.Equal(path.Empty()) { @@ -114,3 +115,46 @@ func ImportStatePassthroughID(ctx context.Context, attrPath path.Path, req Impor resp.Diagnostics.Append(resp.State.SetAttribute(ctx, attrPath, req.ID)...) } } + +// ImportStatePassthroughWithIdentity is a helper function to retrieve either the import identifier +// or a given identity attribute that is then used to set to given attribute path in state, based on the method used +// by the practitioner to import. The identity and state attributes provided must be of type string. +// +// The helper method should only be used on resources that support identity via the resource.ResourceWithIdentity interface. +// +// This method will also automatically pass through the Identity field if imported by +// the identity attribute of a import config block (Terraform 1.12+ and later). +func ImportStatePassthroughWithIdentity(ctx context.Context, stateAttrPath, identityAttrPath path.Path, req ImportStateRequest, resp *ImportStateResponse) { + if stateAttrPath.Equal(path.Empty()) { + resp.Diagnostics.AddError( + "Resource Import Passthrough Missing State Attribute Path", + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Resource ImportState method call to ImportStatePassthroughWithIdentity path must be set to a valid state attribute path that can accept a string value.", + ) + } + + if identityAttrPath.Equal(path.Empty()) { + resp.Diagnostics.AddError( + "Resource Import Passthrough Missing Identity Attribute Path", + "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ + "Resource ImportState method call to ImportStatePassthroughWithIdentity path must be set to a valid identity attribute path that is a string value.", + ) + } + + // If the import is using the import identifier, (either via the "terraform import" CLI command, or a config block with the "id" attribute set) + // pass through the ID to the designated state attribute. + if req.ID != "" { + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, stateAttrPath, req.ID)...) + return + } + + // The import isn't using the import identifier, so it must be using identity. Grab the designated + // identity attribute string and set it to state. + var identityAttrVal types.String + resp.Diagnostics.Append(req.Identity.GetAttribute(ctx, identityAttrPath, &identityAttrVal)...) + if resp.Diagnostics.HasError() { + return + } + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, stateAttrPath, identityAttrVal)...) +} From a56a475fc9be30fb0fab92bea3133cc97c32b9a7 Mon Sep 17 00:00:00 2001 From: Austin Valle Date: Thu, 24 Apr 2025 07:28:46 -0500 Subject: [PATCH 2/2] Update with correct diag returns --- resource/import_state.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/resource/import_state.go b/resource/import_state.go index ab3635fc1..2c8e0ea1a 100644 --- a/resource/import_state.go +++ b/resource/import_state.go @@ -107,6 +107,7 @@ func ImportStatePassthroughID(ctx context.Context, attrPath path.Path, req Impor "This is always an error in the provider. Please report the following to the provider developer:\n\n"+ "Resource ImportState method call to ImportStatePassthroughID path must be set to a valid attribute path that can accept a string value.", ) + return } // If the import is using the ID string identifier, (either via the "terraform import" CLI command, or a config block with the "id" attribute set) @@ -141,6 +142,10 @@ func ImportStatePassthroughWithIdentity(ctx context.Context, stateAttrPath, iden ) } + if resp.Diagnostics.HasError() { + return + } + // If the import is using the import identifier, (either via the "terraform import" CLI command, or a config block with the "id" attribute set) // pass through the ID to the designated state attribute. if req.ID != "" {