Skip to content

Commit b451f6d

Browse files
authored
support importing by resource identity (#1463)
* support importing by resource identity * add todo note * add tests for ImportResourceState with identity * turn todo into comment * add tests for ImportStateWithIdentity * fix logging_http_transport_test.go after terraform.io started redirecting recently
1 parent b277445 commit b451f6d

File tree

7 files changed

+403
-13
lines changed

7 files changed

+403
-13
lines changed

helper/logging/logging_http_transport_test.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestNewLoggingHTTPTransport(t *testing.T) {
3131
reqBody := `An example
3232
multiline
3333
request body`
34-
req, _ := http.NewRequest("GET", "https://www.terraform.io", bytes.NewBufferString(reqBody))
34+
req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", bytes.NewBufferString(reqBody))
3535
res, err := client.Do(req.WithContext(ctx))
3636
if err != nil {
3737
t.Fatalf("request failed: %v", err)
@@ -40,7 +40,7 @@ func TestNewLoggingHTTPTransport(t *testing.T) {
4040

4141
entries, err := tflogtest.MultilineJSONDecode(loggerOutput)
4242
if err != nil {
43-
t.Fatalf("log outtput parsing failed: %v", err)
43+
t.Fatalf("log output parsing failed: %v", err)
4444
}
4545

4646
if len(entries) != 2 {
@@ -67,12 +67,12 @@ func TestNewLoggingHTTPTransport(t *testing.T) {
6767
"@module": "provider",
6868
"tf_http_op_type": "request",
6969
"tf_http_req_method": "GET",
70-
"tf_http_req_uri": "/",
70+
"tf_http_req_uri": "/terraform",
7171
"tf_http_req_version": "HTTP/1.1",
7272
"tf_http_req_body": "An example multiline request body",
7373
"tf_http_trans_id": transId,
7474
"Accept-Encoding": "gzip",
75-
"Host": "www.terraform.io",
75+
"Host": "developer.hashicorp.com",
7676
"User-Agent": "Go-http-client/1.1",
7777
"Content-Length": "37",
7878
}); diff != "" {
@@ -122,7 +122,7 @@ func TestNewSubsystemLoggingHTTPTransport(t *testing.T) {
122122
reqBody := `An example
123123
multiline
124124
request body`
125-
req, _ := http.NewRequest("GET", "https://www.terraform.io", bytes.NewBufferString(reqBody))
125+
req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", bytes.NewBufferString(reqBody))
126126
res, err := client.Do(req.WithContext(ctx))
127127
if err != nil {
128128
t.Fatalf("request failed: %v", err)
@@ -158,12 +158,12 @@ func TestNewSubsystemLoggingHTTPTransport(t *testing.T) {
158158
"@module": "provider.test-subsystem",
159159
"tf_http_op_type": "request",
160160
"tf_http_req_method": "GET",
161-
"tf_http_req_uri": "/",
161+
"tf_http_req_uri": "/terraform",
162162
"tf_http_req_version": "HTTP/1.1",
163163
"tf_http_req_body": "An example multiline request body",
164164
"tf_http_trans_id": transId,
165165
"Accept-Encoding": "gzip",
166-
"Host": "www.terraform.io",
166+
"Host": "developer.hashicorp.com",
167167
"User-Agent": "Go-http-client/1.1",
168168
"Content-Length": "37",
169169
}); diff != "" {
@@ -201,7 +201,7 @@ func TestNewSubsystemLoggingHTTPTransport(t *testing.T) {
201201
func TestNewLoggingHTTPTransport_LogMasking(t *testing.T) {
202202
ctx, loggerOutput := setupRootLogger()
203203
ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "tf_http_op_type")
204-
ctx = tflog.MaskAllFieldValuesRegexes(ctx, regexp.MustCompile(`(?s)<html>.*</html>`))
204+
ctx = tflog.MaskAllFieldValuesRegexes(ctx, regexp.MustCompile(`(?s)<html.*>.*</html>`))
205205
ctx = tflog.MaskMessageStrings(ctx, "Request", "Response")
206206

207207
transport := logging.NewLoggingHTTPTransport(http.DefaultTransport)
@@ -210,7 +210,7 @@ func TestNewLoggingHTTPTransport_LogMasking(t *testing.T) {
210210
Timeout: 10 * time.Second,
211211
}
212212

213-
req, _ := http.NewRequest("GET", "https://www.terraform.io", nil)
213+
req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", nil)
214214
res, err := client.Do(req.WithContext(ctx))
215215
if err != nil {
216216
t.Fatalf("request failed: %v", err)
@@ -266,7 +266,7 @@ func TestNewLoggingHTTPTransport_LogOmitting(t *testing.T) {
266266
Timeout: 10 * time.Second,
267267
}
268268

269-
req, _ := http.NewRequest("GET", "https://www.terraform.io", nil)
269+
req, _ := http.NewRequest("GET", "https://developer.hashicorp.com/terraform", nil)
270270
res, err := client.Do(req.WithContext(ctx))
271271
if err != nil {
272272
t.Fatalf("request failed: %v", err)

helper/schema/grpc_provider.go

Lines changed: 50 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1566,7 +1566,27 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro
15661566
return resp, nil
15671567
}
15681568

1569-
newInstanceStates, err := s.provider.ImportState(ctx, info, req.ID)
1569+
var identity map[string]string
1570+
// parse identity data if available
1571+
if req.Identity != nil && req.Identity.IdentityData != nil {
1572+
// convert req.Identity to flat map identity structure
1573+
// Step 1: Turn JSON into cty.Value based on schema
1574+
identityBlock, err := s.getResourceIdentitySchemaBlock(req.TypeName)
1575+
if err != nil {
1576+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err))
1577+
return resp, nil
1578+
}
1579+
1580+
identityVal, err := msgpack.Unmarshal(req.Identity.IdentityData.MsgPack, identityBlock.ImpliedType())
1581+
if err != nil {
1582+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
1583+
return resp, nil
1584+
}
1585+
// Step 2: Turn cty.Value into flatmap representation
1586+
identity = hcl2shim.FlatmapValueFromHCL2(identityVal)
1587+
}
1588+
1589+
newInstanceStates, err := s.provider.ImportStateWithIdentity(ctx, info, req.ID, identity)
15701590
if err != nil {
15711591
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
15721592
return resp, nil
@@ -1622,12 +1642,40 @@ func (s *GRPCProviderServer) ImportResourceState(ctx context.Context, req *tfpro
16221642
return resp, nil
16231643
}
16241644

1645+
var identityData *tfprotov5.ResourceIdentityData
1646+
if is.Identity != nil {
1647+
identityBlock, err := s.getResourceIdentitySchemaBlock(resourceType)
1648+
if err != nil {
1649+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, fmt.Errorf("getting identity schema failed for resource '%s': %w", req.TypeName, err))
1650+
return resp, nil
1651+
}
1652+
1653+
newIdentityVal, err := hcl2shim.HCL2ValueFromFlatmap(is.Identity, identityBlock.ImpliedType())
1654+
if err != nil {
1655+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
1656+
return resp, nil
1657+
}
1658+
1659+
newIdentityMP, err := msgpack.Marshal(newIdentityVal, identityBlock.ImpliedType())
1660+
if err != nil {
1661+
resp.Diagnostics = convert.AppendProtoDiag(ctx, resp.Diagnostics, err)
1662+
return resp, nil
1663+
}
1664+
1665+
identityData = &tfprotov5.ResourceIdentityData{
1666+
IdentityData: &tfprotov5.DynamicValue{
1667+
MsgPack: newIdentityMP,
1668+
},
1669+
}
1670+
}
1671+
16251672
importedResource := &tfprotov5.ImportedResource{
16261673
TypeName: resourceType,
16271674
State: &tfprotov5.DynamicValue{
16281675
MsgPack: newStateMP,
16291676
},
1630-
Private: meta,
1677+
Private: meta,
1678+
Identity: identityData,
16311679
}
16321680

16331681
resp.ImportedResources = append(resp.ImportedResources, importedResource)

helper/schema/grpc_provider_test.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7715,6 +7715,169 @@ func TestImportResourceState(t *testing.T) {
77157715
},
77167716
},
77177717
},
7718+
"basic-import-from-identity": {
7719+
server: NewGRPCProviderServer(&Provider{
7720+
ResourcesMap: map[string]*Resource{
7721+
"test": {
7722+
SchemaVersion: 1,
7723+
Schema: map[string]*Schema{
7724+
"id": {
7725+
Type: TypeString,
7726+
Required: true,
7727+
},
7728+
"test_string": {
7729+
Type: TypeString,
7730+
Computed: true,
7731+
},
7732+
},
7733+
Identity: &ResourceIdentity{
7734+
Version: 1,
7735+
SchemaFunc: func() map[string]*Schema {
7736+
return map[string]*Schema{
7737+
"id": {
7738+
Type: TypeString,
7739+
RequiredForImport: true,
7740+
},
7741+
}
7742+
},
7743+
},
7744+
Importer: &ResourceImporter{
7745+
StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) {
7746+
identity, err := d.Identity()
7747+
if err != nil {
7748+
t.Fatalf("failed to get identity: %v", err)
7749+
}
7750+
result, exists := identity.GetOk("id")
7751+
if !exists {
7752+
t.Fatalf("expected id to exist in identity")
7753+
}
7754+
7755+
err = d.Set("test_string", "new-imported-val")
7756+
if err != nil {
7757+
return nil, err
7758+
}
7759+
7760+
d.SetId(result.(string))
7761+
7762+
return []*ResourceData{d}, nil
7763+
},
7764+
},
7765+
},
7766+
},
7767+
}),
7768+
req: &tfprotov5.ImportResourceStateRequest{
7769+
TypeName: "test",
7770+
Identity: &tfprotov5.ResourceIdentityData{
7771+
IdentityData: &tfprotov5.DynamicValue{
7772+
MsgPack: mustMsgpackMarshal(
7773+
cty.Object(map[string]cty.Type{
7774+
"id": cty.String,
7775+
}),
7776+
cty.ObjectVal(map[string]cty.Value{
7777+
"id": cty.StringVal("imported-id"),
7778+
}),
7779+
),
7780+
},
7781+
},
7782+
},
7783+
expected: &tfprotov5.ImportResourceStateResponse{
7784+
ImportedResources: []*tfprotov5.ImportedResource{
7785+
{
7786+
TypeName: "test",
7787+
State: &tfprotov5.DynamicValue{
7788+
MsgPack: mustMsgpackMarshal(
7789+
cty.Object(map[string]cty.Type{
7790+
"id": cty.String,
7791+
"test_string": cty.String,
7792+
}),
7793+
cty.ObjectVal(map[string]cty.Value{
7794+
"id": cty.StringVal("imported-id"),
7795+
"test_string": cty.StringVal("new-imported-val"),
7796+
}),
7797+
),
7798+
},
7799+
Private: []byte(`{"schema_version":"1"}`),
7800+
Identity: &tfprotov5.ResourceIdentityData{
7801+
IdentityData: &tfprotov5.DynamicValue{
7802+
MsgPack: mustMsgpackMarshal(
7803+
cty.Object(map[string]cty.Type{
7804+
"id": cty.String,
7805+
}),
7806+
cty.ObjectVal(map[string]cty.Value{
7807+
"id": cty.StringVal("imported-id"),
7808+
}),
7809+
),
7810+
},
7811+
},
7812+
},
7813+
},
7814+
},
7815+
},
7816+
"basic-import-from-identity-no-id": {
7817+
server: NewGRPCProviderServer(&Provider{
7818+
ResourcesMap: map[string]*Resource{
7819+
"test": {
7820+
SchemaVersion: 1,
7821+
Schema: map[string]*Schema{
7822+
"id": {
7823+
Type: TypeString,
7824+
Required: true,
7825+
},
7826+
"test_string": {
7827+
Type: TypeString,
7828+
Computed: true,
7829+
},
7830+
},
7831+
Identity: &ResourceIdentity{
7832+
Version: 1,
7833+
SchemaFunc: func() map[string]*Schema {
7834+
return map[string]*Schema{
7835+
"id": {
7836+
Type: TypeString,
7837+
RequiredForImport: true,
7838+
},
7839+
}
7840+
},
7841+
},
7842+
Importer: &ResourceImporter{
7843+
// Note: this does not set the Id on the ResourceData which results in an error that this test expects
7844+
StateContext: func(ctx context.Context, d *ResourceData, meta interface{}) ([]*ResourceData, error) {
7845+
err := d.Set("test_string", "new-imported-val")
7846+
if err != nil {
7847+
return nil, err
7848+
}
7849+
7850+
return []*ResourceData{d}, nil
7851+
},
7852+
},
7853+
},
7854+
},
7855+
}),
7856+
req: &tfprotov5.ImportResourceStateRequest{
7857+
TypeName: "test",
7858+
Identity: &tfprotov5.ResourceIdentityData{
7859+
IdentityData: &tfprotov5.DynamicValue{
7860+
MsgPack: mustMsgpackMarshal(
7861+
cty.Object(map[string]cty.Type{
7862+
"id": cty.String,
7863+
}),
7864+
cty.ObjectVal(map[string]cty.Value{
7865+
"id": cty.StringVal("imported-id"),
7866+
}),
7867+
),
7868+
},
7869+
},
7870+
},
7871+
expected: &tfprotov5.ImportResourceStateResponse{
7872+
ImportedResources: nil,
7873+
Diagnostics: []*tfprotov5.Diagnostic{
7874+
{
7875+
Severity: tfprotov5.DiagnosticSeverityError,
7876+
Summary: "The provider returned a resource missing an identifier during ImportResourceState. This is generally a bug in the resource implementation for import. Resource import code should not call d.SetId(\"\") or create an empty ResourceData. If the resource is missing, instead return an error. Please report this to the provider developers.",
7877+
},
7878+
},
7879+
},
7880+
},
77187881
"resource-doesnt-exist": {
77197882
server: NewGRPCProviderServer(&Provider{
77207883
ResourcesMap: map[string]*Resource{

helper/schema/provider.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,14 @@ func (p *Provider) ImportState(
476476
ctx context.Context,
477477
info *terraform.InstanceInfo,
478478
id string) ([]*terraform.InstanceState, error) {
479+
return p.ImportStateWithIdentity(ctx, info, id, nil)
480+
}
481+
482+
func (p *Provider) ImportStateWithIdentity(
483+
ctx context.Context,
484+
info *terraform.InstanceInfo,
485+
id string,
486+
identity map[string]string) ([]*terraform.InstanceState, error) {
479487
// Find the resource
480488
r, ok := p.ResourcesMap[info.Type]
481489
if !ok {
@@ -492,6 +500,16 @@ func (p *Provider) ImportState(
492500
data.SetId(id)
493501
data.SetType(info.Type)
494502

503+
if data.identitySchema != nil {
504+
identityData, err := data.Identity()
505+
if err != nil {
506+
return nil, err // this should not happen, as we checked above
507+
}
508+
identityData.raw = identity // is this too hacky / unexpected?
509+
} else if identity != nil {
510+
return nil, fmt.Errorf("resource %s doesn't support identity import", info.Type)
511+
}
512+
495513
// Call the import function
496514
results := []*ResourceData{data}
497515
if r.Importer.State != nil || r.Importer.StateContext != nil {

0 commit comments

Comments
 (0)