Skip to content

Commit ce3618e

Browse files
committed
initial import implementation with some TODOs
1 parent 8ab70d0 commit ce3618e

15 files changed

+780
-9
lines changed

internal/fromproto5/importresourcestate.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818

1919
// ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest
2020
// equivalent of a *tfprotov5.ImportResourceStateRequest.
21-
func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
21+
func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
2222
if proto5 == nil {
2323
return nil, nil
2424
}
@@ -45,10 +45,17 @@ func ImportResourceStateRequest(ctx context.Context, proto5 *tfprotov5.ImportRes
4545
Schema: resourceSchema,
4646
},
4747
ID: proto5.ID,
48+
IdentitySchema: identitySchema,
4849
Resource: reqResource,
4950
TypeName: proto5.TypeName,
5051
ClientCapabilities: ImportStateClientCapabilities(proto5.ClientCapabilities),
5152
}
5253

54+
identity, identityDiags := ResourceIdentity(ctx, proto5.Identity, identitySchema)
55+
56+
diags.Append(identityDiags...)
57+
58+
fw.Identity = identity
59+
5360
return fw, diags
5461
}

internal/fromproto5/importresourcestate_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
1717
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
1818
"github.com/hashicorp/terraform-plugin-framework/resource"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
1920
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2021
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
2122
)
@@ -31,6 +32,30 @@ func TestImportResourceStateRequest(t *testing.T) {
3132
},
3233
}
3334

35+
testIdentityProto5Type := tftypes.Object{
36+
AttributeTypes: map[string]tftypes.Type{
37+
"test_identity_attribute": tftypes.String,
38+
},
39+
}
40+
41+
testIdentityProto5Value := tftypes.NewValue(testIdentityProto5Type, map[string]tftypes.Value{
42+
"test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"),
43+
})
44+
45+
testIdentityProto5DynamicValue, err := tfprotov5.NewDynamicValue(testIdentityProto5Type, testIdentityProto5Value)
46+
47+
if err != nil {
48+
t.Fatalf("unexpected error calling tfprotov5.NewDynamicValue(): %s", err)
49+
}
50+
51+
testIdentitySchema := identityschema.Schema{
52+
Attributes: map[string]identityschema.Attribute{
53+
"test_identity_attribute": identityschema.StringAttribute{
54+
RequiredForImport: true,
55+
},
56+
},
57+
}
58+
3459
testFwEmptyState := tfsdk.State{
3560
Raw: tftypes.NewValue(testFwSchema.Type().TerraformType(context.Background()), nil),
3661
Schema: testFwSchema,
@@ -39,6 +64,7 @@ func TestImportResourceStateRequest(t *testing.T) {
3964
testCases := map[string]struct {
4065
input *tfprotov5.ImportResourceStateRequest
4166
resourceSchema fwschema.Schema
67+
identitySchema fwschema.Schema
4268
resource resource.Resource
4369
expected *fwserver.ImportResourceStateRequest
4470
expectedDiagnostics diag.Diagnostics
@@ -67,6 +93,42 @@ func TestImportResourceStateRequest(t *testing.T) {
6793
),
6894
},
6995
},
96+
"identity-missing-schema": {
97+
input: &tfprotov5.ImportResourceStateRequest{
98+
Identity: &tfprotov5.ResourceIdentityData{
99+
IdentityData: &testIdentityProto5DynamicValue,
100+
},
101+
},
102+
resourceSchema: testFwSchema,
103+
expected: &fwserver.ImportResourceStateRequest{
104+
EmptyState: testFwEmptyState,
105+
},
106+
expectedDiagnostics: diag.Diagnostics{
107+
diag.NewErrorDiagnostic(
108+
"Unable to Convert Resource Identity",
109+
"An unexpected error was encountered when converting the resource identity from the protocol type. "+
110+
"Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+
111+
"This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.",
112+
),
113+
},
114+
},
115+
"identity": {
116+
input: &tfprotov5.ImportResourceStateRequest{
117+
Identity: &tfprotov5.ResourceIdentityData{
118+
IdentityData: &testIdentityProto5DynamicValue,
119+
},
120+
},
121+
resourceSchema: testFwSchema,
122+
identitySchema: testIdentitySchema,
123+
expected: &fwserver.ImportResourceStateRequest{
124+
EmptyState: testFwEmptyState,
125+
IdentitySchema: testIdentitySchema,
126+
Identity: &tfsdk.ResourceIdentity{
127+
Raw: testIdentityProto5Value,
128+
Schema: testIdentitySchema,
129+
},
130+
},
131+
},
70132
"id": {
71133
input: &tfprotov5.ImportResourceStateRequest{
72134
ID: "test-id",
@@ -122,7 +184,7 @@ func TestImportResourceStateRequest(t *testing.T) {
122184
t.Run(name, func(t *testing.T) {
123185
t.Parallel()
124186

125-
got, diags := fromproto5.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema)
187+
got, diags := fromproto5.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema)
126188

127189
if diff := cmp.Diff(got, testCase.expected); diff != "" {
128190
t.Errorf("unexpected difference: %s", diff)

internal/fromproto6/importresourcestate.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818

1919
// ImportResourceStateRequest returns the *fwserver.ImportResourceStateRequest
2020
// equivalent of a *tfprotov6.ImportResourceStateRequest.
21-
func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
21+
func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportResourceStateRequest, reqResource resource.Resource, resourceSchema fwschema.Schema, identitySchema fwschema.Schema) (*fwserver.ImportResourceStateRequest, diag.Diagnostics) {
2222
if proto6 == nil {
2323
return nil, nil
2424
}
@@ -45,10 +45,17 @@ func ImportResourceStateRequest(ctx context.Context, proto6 *tfprotov6.ImportRes
4545
Schema: resourceSchema,
4646
},
4747
ID: proto6.ID,
48+
IdentitySchema: identitySchema,
4849
Resource: reqResource,
4950
TypeName: proto6.TypeName,
5051
ClientCapabilities: ImportStateClientCapabilities(proto6.ClientCapabilities),
5152
}
5253

54+
identity, identityDiags := ResourceIdentity(ctx, proto6.Identity, identitySchema)
55+
56+
diags.Append(identityDiags...)
57+
58+
fw.Identity = identity
59+
5360
return fw, diags
5461
}

internal/fromproto6/importresourcestate_test.go

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
1717
"github.com/hashicorp/terraform-plugin-framework/internal/fwserver"
1818
"github.com/hashicorp/terraform-plugin-framework/resource"
19+
"github.com/hashicorp/terraform-plugin-framework/resource/identityschema"
1920
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
2021
"github.com/hashicorp/terraform-plugin-framework/tfsdk"
2122
)
@@ -31,6 +32,30 @@ func TestImportResourceStateRequest(t *testing.T) {
3132
},
3233
}
3334

35+
testIdentityProto6Type := tftypes.Object{
36+
AttributeTypes: map[string]tftypes.Type{
37+
"test_identity_attribute": tftypes.String,
38+
},
39+
}
40+
41+
testIdentityProto6Value := tftypes.NewValue(testIdentityProto6Type, map[string]tftypes.Value{
42+
"test_identity_attribute": tftypes.NewValue(tftypes.String, "id-123"),
43+
})
44+
45+
testIdentityProto6DynamicValue, err := tfprotov6.NewDynamicValue(testIdentityProto6Type, testIdentityProto6Value)
46+
47+
if err != nil {
48+
t.Fatalf("unexpected error calling tfprotov6.NewDynamicValue(): %s", err)
49+
}
50+
51+
testIdentitySchema := identityschema.Schema{
52+
Attributes: map[string]identityschema.Attribute{
53+
"test_identity_attribute": identityschema.StringAttribute{
54+
RequiredForImport: true,
55+
},
56+
},
57+
}
58+
3459
testFwEmptyState := tfsdk.State{
3560
Raw: tftypes.NewValue(testFwSchema.Type().TerraformType(context.Background()), nil),
3661
Schema: testFwSchema,
@@ -39,6 +64,7 @@ func TestImportResourceStateRequest(t *testing.T) {
3964
testCases := map[string]struct {
4065
input *tfprotov6.ImportResourceStateRequest
4166
resourceSchema fwschema.Schema
67+
identitySchema fwschema.Schema
4268
resource resource.Resource
4369
expected *fwserver.ImportResourceStateRequest
4470
expectedDiagnostics diag.Diagnostics
@@ -67,6 +93,42 @@ func TestImportResourceStateRequest(t *testing.T) {
6793
),
6894
},
6995
},
96+
"identity-missing-schema": {
97+
input: &tfprotov6.ImportResourceStateRequest{
98+
Identity: &tfprotov6.ResourceIdentityData{
99+
IdentityData: &testIdentityProto6DynamicValue,
100+
},
101+
},
102+
resourceSchema: testFwSchema,
103+
expected: &fwserver.ImportResourceStateRequest{
104+
EmptyState: testFwEmptyState,
105+
},
106+
expectedDiagnostics: diag.Diagnostics{
107+
diag.NewErrorDiagnostic(
108+
"Unable to Convert Resource Identity",
109+
"An unexpected error was encountered when converting the resource identity from the protocol type. "+
110+
"Identity data was sent in the protocol to a resource that doesn't support identity.\n\n"+
111+
"This is always a problem with Terraform or terraform-plugin-framework. Please report this to the provider developer.",
112+
),
113+
},
114+
},
115+
"identity": {
116+
input: &tfprotov6.ImportResourceStateRequest{
117+
Identity: &tfprotov6.ResourceIdentityData{
118+
IdentityData: &testIdentityProto6DynamicValue,
119+
},
120+
},
121+
resourceSchema: testFwSchema,
122+
identitySchema: testIdentitySchema,
123+
expected: &fwserver.ImportResourceStateRequest{
124+
EmptyState: testFwEmptyState,
125+
IdentitySchema: testIdentitySchema,
126+
Identity: &tfsdk.ResourceIdentity{
127+
Raw: testIdentityProto6Value,
128+
Schema: testIdentitySchema,
129+
},
130+
},
131+
},
70132
"id": {
71133
input: &tfprotov6.ImportResourceStateRequest{
72134
ID: "test-id",
@@ -122,7 +184,7 @@ func TestImportResourceStateRequest(t *testing.T) {
122184
t.Run(name, func(t *testing.T) {
123185
t.Parallel()
124186

125-
got, diags := fromproto6.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema)
187+
got, diags := fromproto6.ImportResourceStateRequest(context.Background(), testCase.input, testCase.resource, testCase.resourceSchema, testCase.identitySchema)
126188

127189
if diff := cmp.Diff(got, testCase.expected); diff != "" {
128190
t.Errorf("unexpected difference: %s", diff)

internal/fwserver/server_importresourcestate.go

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/hashicorp/terraform-plugin-go/tftypes"
1010

1111
"github.com/hashicorp/terraform-plugin-framework/diag"
12+
"github.com/hashicorp/terraform-plugin-framework/internal/fwschema"
1213
"github.com/hashicorp/terraform-plugin-framework/internal/logging"
1314
"github.com/hashicorp/terraform-plugin-framework/internal/privatestate"
1415
"github.com/hashicorp/terraform-plugin-framework/resource"
@@ -18,14 +19,29 @@ import (
1819
// ImportedResource represents a resource that was imported.
1920
type ImportedResource struct {
2021
Private *privatestate.Data
22+
Identity *tfsdk.ResourceIdentity
2123
State tfsdk.State
2224
TypeName string
2325
}
2426

2527
// ImportResourceStateRequest is the framework server request for the
2628
// ImportResourceState RPC.
29+
//
30+
// Either ID or Identity will be supplied depending on how the resource is being imported.
2731
type ImportResourceStateRequest struct {
28-
ID string
32+
// ID will come from the import CLI command or an import config block with the "id" attribute assigned.
33+
//
34+
// This ID field is a special string identifier that can be parsed however the provider deems fit.
35+
ID string
36+
37+
// Identity will come from an import config block with the "identity" attribute assigned and will conform
38+
// to the identity schema defined by the resource. (Terraform v1.12+)
39+
//
40+
// All attributes marked as RequiredForImport will be populated (enforced by Terraform core) and OptionalForImport
41+
// attributes may be null, but could have a config value.
42+
Identity *tfsdk.ResourceIdentity
43+
IdentitySchema fwschema.Schema
44+
2945
Resource resource.Resource
3046

3147
// EmptyState is an empty State for the resource schema. This is used to
@@ -132,6 +148,29 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta
132148
Private: privateProviderData,
133149
}
134150

151+
// If the resource supports identity and we are not importing by identity, pre-populate with a null value.
152+
// TODO:ResourceIdentity: Is there any reason a provider WOULD NOT want to populate an identity when it supports one?
153+
if req.Identity == nil && req.IdentitySchema != nil {
154+
nullTfValue := tftypes.NewValue(req.IdentitySchema.Type().TerraformType(ctx), nil)
155+
156+
req.Identity = &tfsdk.ResourceIdentity{
157+
Schema: req.IdentitySchema,
158+
Raw: nullTfValue.Copy(),
159+
}
160+
}
161+
162+
if req.Identity != nil {
163+
importReq.Identity = &tfsdk.ResourceIdentity{
164+
Schema: req.Identity.Schema,
165+
Raw: req.Identity.Raw.Copy(),
166+
}
167+
168+
importResp.Identity = &tfsdk.ResourceIdentity{
169+
Schema: req.Identity.Schema,
170+
Raw: req.Identity.Raw.Copy(),
171+
}
172+
}
173+
135174
logging.FrameworkTrace(ctx, "Calling provider defined Resource ImportState")
136175
resourceWithImportState.ImportState(ctx, importReq, &importResp)
137176
logging.FrameworkTrace(ctx, "Called provider defined Resource ImportState")
@@ -154,7 +193,11 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta
154193

155194
importResp.State.Raw = modifiedState
156195

157-
if importResp.State.Raw.Equal(req.EmptyState.Raw) {
196+
// TODO:ResourceIdentity: Now you can reasonably import using just the identity field and no state. However this still feels like not a great idea, because it's possible
197+
// to import via ID with a client that doesn't support identity, so the "Read" logic for providers will always have to account for both scenarios.
198+
//
199+
// A potential improvement on this could be to check if identity AND state are empty. That is the only true error state because then no data would be transferred from import to read.
200+
if req.ID != "" && importResp.State.Raw.Equal(req.EmptyState.Raw) {
158201
resp.Diagnostics.AddError(
159202
"Missing Resource Import State",
160203
"An unexpected error was encountered when importing the resource. This is always a problem with the provider. Please give the following information to the provider developer:\n\n"+
@@ -169,10 +212,21 @@ func (s *Server) ImportResourceState(ctx context.Context, req *ImportResourceSta
169212
private.Provider = importResp.Private
170213
}
171214

215+
if importResp.Identity != nil && req.IdentitySchema == nil {
216+
resp.Diagnostics.AddError(
217+
"Unexpected ImportState Response",
218+
"An unexpected error was encountered when creating the import response. New identity data was returned by the provider import operation, but the resource does not indicate identity support.\n\n"+
219+
"This is always a problem with the provider and should be reported to the provider developer.",
220+
)
221+
222+
return
223+
}
224+
172225
resp.Deferred = importResp.Deferred
173226
resp.ImportedResources = []ImportedResource{
174227
{
175228
State: importResp.State,
229+
Identity: importResp.Identity,
176230
TypeName: req.TypeName,
177231
Private: private,
178232
},

internal/proto5server/server_importresourcestate.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,15 @@ func (s *Server) ImportResourceState(ctx context.Context, proto5Req *tfprotov5.I
3636
return toproto5.ImportResourceStateResponse(ctx, fwResp), nil
3737
}
3838

39-
fwReq, diags := fromproto5.ImportResourceStateRequest(ctx, proto5Req, resource, resourceSchema)
39+
identitySchema, diags := s.FrameworkServer.ResourceIdentitySchema(ctx, proto5Req.TypeName)
40+
41+
fwResp.Diagnostics.Append(diags...)
42+
43+
if fwResp.Diagnostics.HasError() {
44+
return toproto5.ImportResourceStateResponse(ctx, fwResp), nil
45+
}
46+
47+
fwReq, diags := fromproto5.ImportResourceStateRequest(ctx, proto5Req, resource, resourceSchema, identitySchema)
4048

4149
fwResp.Diagnostics.Append(diags...)
4250

0 commit comments

Comments
 (0)