Skip to content

Commit 2456d71

Browse files
committed
Import block works for a resource with a dependency
1 parent 91b5a0c commit 2456d71

File tree

4 files changed

+296
-23
lines changed

4 files changed

+296
-23
lines changed

helper/resource/importstate/examplecloud_test.go

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
package importstate_test
55

66
import (
7+
"context"
8+
79
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
810
"github.com/hashicorp/terraform-plugin-go/tftypes"
911
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
@@ -116,3 +118,154 @@ func examplecloudResource() testprovider.Resource {
116118
},
117119
}
118120
}
121+
122+
// examplecloudZone is a test resource that mimics a DNS zone resource.
123+
func examplecloudZone() testprovider.Resource {
124+
return testprovider.Resource{
125+
CreateResponse: &resource.CreateResponse{
126+
NewState: tftypes.NewValue(
127+
tftypes.Object{
128+
AttributeTypes: map[string]tftypes.Type{
129+
"id": tftypes.String,
130+
"name": tftypes.String,
131+
},
132+
},
133+
map[string]tftypes.Value{
134+
"id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
135+
"name": tftypes.NewValue(tftypes.String, "example.net"),
136+
},
137+
),
138+
},
139+
ReadResponse: &resource.ReadResponse{
140+
NewState: tftypes.NewValue(
141+
tftypes.Object{
142+
AttributeTypes: map[string]tftypes.Type{
143+
"id": tftypes.String,
144+
"name": tftypes.String,
145+
},
146+
},
147+
map[string]tftypes.Value{
148+
"id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
149+
"name": tftypes.NewValue(tftypes.String, "example.net"),
150+
},
151+
),
152+
},
153+
ImportStateResponse: &resource.ImportStateResponse{
154+
State: tftypes.NewValue(
155+
tftypes.Object{
156+
AttributeTypes: map[string]tftypes.Type{
157+
"id": tftypes.String,
158+
"name": tftypes.String,
159+
},
160+
},
161+
map[string]tftypes.Value{
162+
"id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
163+
"name": tftypes.NewValue(tftypes.String, "example.net"),
164+
},
165+
),
166+
},
167+
SchemaResponse: &resource.SchemaResponse{
168+
Schema: &tfprotov6.Schema{
169+
Block: &tfprotov6.SchemaBlock{
170+
Attributes: []*tfprotov6.SchemaAttribute{
171+
{
172+
Name: "id",
173+
Type: tftypes.String,
174+
Computed: true,
175+
},
176+
{
177+
Name: "name",
178+
Type: tftypes.String,
179+
Required: true,
180+
},
181+
},
182+
},
183+
},
184+
},
185+
}
186+
}
187+
188+
// examplecloudZoneRecord is a test resource that mimics a DNS zone record resource.
189+
// It models a resource dependency; specifically, it depends on a DNS zone ID and will
190+
// plan a replacement if the zone ID changes.
191+
func examplecloudZoneRecord() testprovider.Resource {
192+
return testprovider.Resource{
193+
CreateResponse: &resource.CreateResponse{
194+
NewState: tftypes.NewValue(
195+
tftypes.Object{
196+
AttributeTypes: map[string]tftypes.Type{
197+
"id": tftypes.String,
198+
"zone_id": tftypes.String,
199+
"name": tftypes.String,
200+
},
201+
},
202+
map[string]tftypes.Value{
203+
"id": tftypes.NewValue(tftypes.String, "f00911be-e188-433d-9ccd-d0393a9f5d05"),
204+
"zone_id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
205+
"name": tftypes.NewValue(tftypes.String, "www"),
206+
},
207+
),
208+
},
209+
210+
PlanChangeFunc: func(ctx context.Context, req resource.PlanChangeRequest, resp *resource.PlanChangeResponse) {
211+
resp.RequiresReplace = []*tftypes.AttributePath{
212+
tftypes.NewAttributePath().WithAttributeName("zone_id"),
213+
}
214+
},
215+
ReadResponse: &resource.ReadResponse{
216+
NewState: tftypes.NewValue(
217+
tftypes.Object{
218+
AttributeTypes: map[string]tftypes.Type{
219+
"id": tftypes.String,
220+
"zone_id": tftypes.String,
221+
"name": tftypes.String,
222+
},
223+
},
224+
map[string]tftypes.Value{
225+
"id": tftypes.NewValue(tftypes.String, "f00911be-e188-433d-9ccd-d0393a9f5d05"),
226+
"zone_id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
227+
"name": tftypes.NewValue(tftypes.String, "www"),
228+
},
229+
),
230+
},
231+
ImportStateResponse: &resource.ImportStateResponse{
232+
State: tftypes.NewValue(
233+
tftypes.Object{
234+
AttributeTypes: map[string]tftypes.Type{
235+
"id": tftypes.String,
236+
"zone_id": tftypes.String,
237+
"name": tftypes.String,
238+
},
239+
},
240+
map[string]tftypes.Value{
241+
"id": tftypes.NewValue(tftypes.String, "f00911be-e188-433d-9ccd-d0393a9f5d05"),
242+
"zone_id": tftypes.NewValue(tftypes.String, "5381dd14-6d75-4f32-9096-47f5500b1507"),
243+
"name": tftypes.NewValue(tftypes.String, "www"),
244+
},
245+
),
246+
},
247+
SchemaResponse: &resource.SchemaResponse{
248+
Schema: &tfprotov6.Schema{
249+
Block: &tfprotov6.SchemaBlock{
250+
Attributes: []*tfprotov6.SchemaAttribute{
251+
{
252+
Name: "id",
253+
Type: tftypes.String,
254+
Computed: true,
255+
},
256+
{
257+
Name: "zone_id",
258+
Type: tftypes.String,
259+
Required: true,
260+
},
261+
{
262+
Name: "name",
263+
Type: tftypes.String,
264+
Required: true,
265+
},
266+
},
267+
},
268+
},
269+
},
270+
}
271+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright (c) HashiCorp, Inc.
2+
// SPDX-License-Identifier: MPL-2.0
3+
4+
package importstate_test
5+
6+
import (
7+
"testing"
8+
9+
"github.com/hashicorp/terraform-plugin-go/tfprotov6"
10+
r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
11+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
12+
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver"
13+
"github.com/hashicorp/terraform-plugin-testing/tfversion"
14+
)
15+
16+
func TestImportBlockForResourceWithADependency(t *testing.T) {
17+
t.Parallel()
18+
19+
config := `
20+
resource "examplecloud_zone" "zone" {
21+
name = "example.net"
22+
}
23+
24+
resource "examplecloud_zone_record" "record" {
25+
zone_id = examplecloud_zone.zone.id
26+
name = "www"
27+
}
28+
`
29+
r.UnitTest(t, r.TestCase{
30+
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
31+
tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later
32+
},
33+
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
34+
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
35+
Resources: map[string]testprovider.Resource{
36+
"examplecloud_zone": examplecloudZone(),
37+
"examplecloud_zone_record": examplecloudZoneRecord(),
38+
},
39+
}),
40+
},
41+
Steps: []r.TestStep{
42+
{
43+
Config: config,
44+
},
45+
{
46+
ImportState: true,
47+
ImportStateKind: r.ImportBlockWithID,
48+
ResourceName: "examplecloud_zone_record.record",
49+
},
50+
},
51+
})
52+
}

helper/resource/testing_new_import_state.go

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -144,14 +144,30 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
144144
importWd = wd
145145
} else {
146146
importWd = helper.RequireNewWorkingDir(ctx, t, "")
147-
defer importWd.Close() //nolint:errcheck
147+
defer importWd.Close()
148148
}
149149

150150
err = importWd.SetConfig(ctx, testStepConfig, step.ConfigVariables)
151151
if err != nil {
152152
t.Fatalf("Error setting test config: %s", err)
153153
}
154154

155+
if kind.plannable() {
156+
if stepNumber > 1 {
157+
err = importWd.CopyState(ctx, wd.StateFilePath())
158+
if err != nil {
159+
t.Fatalf("copying state: %s", err)
160+
}
161+
162+
err = runProviderCommand(ctx, t, func() error {
163+
return importWd.RemoveResource(ctx, resourceName)
164+
}, importWd, providers)
165+
if err != nil {
166+
t.Fatalf("removing resource %s from copied state: %s", resourceName, err)
167+
}
168+
}
169+
}
170+
155171
if !importStatePersist {
156172
err = runProviderCommand(ctx, t, func() error {
157173
logging.HelperResourceDebug(ctx, "Run terraform init")
@@ -184,35 +200,37 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
184200
return err
185201
}
186202

187-
if plan.ResourceChanges != nil {
188-
logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges)))
203+
logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges)))
189204

190-
for _, rc := range plan.ResourceChanges {
191-
if rc.Address != resourceName {
192-
// we're only interested in the changes for the resource being imported
193-
continue
194-
}
195-
if rc.Change != nil && rc.Change.Actions != nil {
196-
// should this be length checked and used as a condition, if it's a no-op then there shouldn't be any other changes here
197-
for _, action := range rc.Change.Actions {
198-
if action != "no-op" {
199-
var stdout string
200-
err = runProviderCommand(ctx, t, func() error {
201-
var err error
202-
stdout, err = importWd.SavedPlanRawStdout(ctx)
203-
return err
204-
}, importWd, providers)
205-
if err != nil {
206-
return fmt.Errorf("retrieving formatted plan output: %w", err)
207-
}
208-
209-
return fmt.Errorf("importing resource %s: expected a no-op resource action, got %q action with plan \nstdout:\n\n%s", rc.Address, action, stdout)
205+
for _, rc := range plan.ResourceChanges {
206+
if rc.Address != resourceName {
207+
// we're only interested in the changes for the resource being imported
208+
continue
209+
}
210+
if rc.Change != nil && rc.Change.Actions != nil {
211+
// should this be length checked and used as a condition, if it's a no-op then there shouldn't be any other changes here
212+
for _, action := range rc.Change.Actions {
213+
if action != "no-op" {
214+
var stdout string
215+
err = runProviderCommand(ctx, t, func() error {
216+
var err error
217+
stdout, err = importWd.SavedPlanRawStdout(ctx)
218+
return err
219+
}, importWd, providers)
220+
if err != nil {
221+
return fmt.Errorf("retrieving formatted plan output: %w", err)
210222
}
223+
224+
return fmt.Errorf("importing resource %s: expected a no-op resource action, got %q action with plan \nstdout:\n\n%s", rc.Address, action, stdout)
211225
}
212226
}
213227
}
214228
}
215229

230+
if len(plan.ResourceChanges) == 0 {
231+
return fmt.Errorf("importing resource %s: expected a resource change, got no changes", resourceName)
232+
}
233+
216234
// TODO compare plan to state from previous step
217235

218236
if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil {

internal/plugintest/working_dir.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package plugintest
66
import (
77
"context"
88
"fmt"
9+
"io"
910
"os"
1011
"path/filepath"
1112

@@ -173,6 +174,40 @@ func (wd *WorkingDir) ClearState(ctx context.Context) error {
173174
return nil
174175
}
175176

177+
func (wd *WorkingDir) CopyState(ctx context.Context, src string) error {
178+
srcState, err := os.Open(src)
179+
if err != nil {
180+
return fmt.Errorf("failed to open statefile for read: %w", err)
181+
}
182+
183+
defer srcState.Close()
184+
185+
dstState, err := os.Create(filepath.Join(wd.baseDir, "terraform.tfstate"))
186+
if err != nil {
187+
return fmt.Errorf("failed to open statefile for write: %w", err)
188+
}
189+
190+
defer dstState.Close()
191+
192+
buf := make([]byte, 1024)
193+
for {
194+
n, err := srcState.Read(buf)
195+
if err != nil {
196+
if err == io.EOF {
197+
break
198+
}
199+
return fmt.Errorf("failed to read from statefile: %w", err)
200+
}
201+
202+
_, err = dstState.Write(buf[:n])
203+
if err != nil {
204+
return fmt.Errorf("failed to write to statefile: %w", err)
205+
}
206+
}
207+
208+
return nil
209+
}
210+
176211
// ClearPlan deletes any saved plan present in the working directory.
177212
func (wd *WorkingDir) ClearPlan(ctx context.Context) error {
178213
logging.HelperResourceTrace(ctx, "Clearing Terraform plan")
@@ -295,6 +330,17 @@ func (wd *WorkingDir) HasSavedPlan() bool {
295330
return err == nil
296331
}
297332

333+
// RemoveResource removes a resource from state.
334+
func (wd *WorkingDir) RemoveResource(ctx context.Context, address string) error {
335+
logging.HelperResourceTrace(ctx, "Calling Terraform CLI state rm command")
336+
337+
err := wd.tf.StateRm(context.Background(), address)
338+
339+
logging.HelperResourceTrace(ctx, "Called Terraform CLI state rm command")
340+
341+
return err
342+
}
343+
298344
// SavedPlan returns an object describing the current saved plan file, if any.
299345
//
300346
// If no plan is saved or if the plan file cannot be read, SavedPlan returns
@@ -349,6 +395,10 @@ func (wd *WorkingDir) State(ctx context.Context) (*tfjson.State, error) {
349395
return state, err
350396
}
351397

398+
func (wd *WorkingDir) StateFilePath() string {
399+
return filepath.Join(wd.baseDir, "terraform.tfstate")
400+
}
401+
352402
// Import runs terraform import
353403
func (wd *WorkingDir) Import(ctx context.Context, resource, id string) error {
354404
logging.HelperResourceTrace(ctx, "Calling Terraform CLI import command")

0 commit comments

Comments
 (0)