Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2e6c321
Prepare to move test to a directory
bbasata Mar 20, 2025
288ec26
Move test to import_test package
bbasata Mar 20, 2025
e924715
Extract testprovider resource from test
bbasata Mar 20, 2025
4b76aba
More test provider extraction
bbasata Mar 20, 2025
f7076a9
Add a test expecting fail when plannable import is not supported
bbasata Mar 20, 2025
6865074
importstate: a working example of ImportState as the first TestStep
bbasata Mar 21, 2025
70f740d
importstate: a working example of ImportBlockWithId as the first Test…
bbasata Mar 21, 2025
d8f5ccb
make generate
bbasata Mar 24, 2025
5e5f6d6
Merge remote-tracking branch 'origin/main' into import-as-first-test-…
bbasata Mar 24, 2025
0872e07
Use PlanChecks
bbasata Mar 24, 2025
5d6bf83
Simpler test names
bbasata Mar 24, 2025
f45d8fe
Tidy comment
bbasata Mar 24, 2025
a36cde2
wip
bbasata Mar 24, 2025
3b822d6
fixup! wip
bbasata Mar 24, 2025
3134be2
Extract functions
bbasata Mar 24, 2025
99f1fae
Add tests for import block in ConfigFile and ConfigDirectory
bbasata Mar 24, 2025
42f36b7
Mark test helper function
bbasata Mar 24, 2025
024ac49
minimize diff
bbasata Mar 24, 2025
bd29ab0
Extract requireNoopResourceAction
bbasata Mar 24, 2025
c0fd026
Fail ImportStateVerify if no new resources are imported
bbasata Mar 24, 2025
b0a502e
ImportPlanVerify
bbasata Mar 25, 2025
4cf6a87
Use ImportPlanVerify
bbasata Mar 25, 2025
f085ea6
fixup! Use ImportPlanVerify
bbasata Mar 25, 2025
eb4679f
fixup! fixup! Use ImportPlanVerify
bbasata Mar 25, 2025
3a7afd5
fixup! fixup! fixup! Use ImportPlanVerify
bbasata Mar 25, 2025
f5f93c1
fixup! fixup! fixup! fixup! Use ImportPlanVerify
bbasata Mar 25, 2025
58ec6b8
fixup! fixup! fixup! fixup! fixup! Use ImportPlanVerify
bbasata Mar 26, 2025
9643933
Update helper/resource/testing.go
bbasata Mar 27, 2025
f9f6ac4
Update helper/resource/testing_new_import_state.go
bbasata Mar 27, 2025
2b23914
Merge branch 'main' into import-compare-plan-to-previous-state
bbasata Mar 31, 2025
c3a6290
helper/resource: easy access to state in tfjson.State form
bbasata Mar 31, 2025
b0abb5b
Merge remote-tracking branch 'origin/get-state-json' into import-comp…
bbasata Mar 31, 2025
8242c05
Use tfjson.State
bbasata Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,12 +38,10 @@ func Test_ImportBlock_AsFirstStep(t *testing.T) {
ImportStateId: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
// ImportStateVerify: true,
Config: `resource "examplecloud_container" "test" {
name = "somevalue"
location = "westeurope"
}`,
ImportStatePersist: true,
ImportPlanChecks: r.ImportPlanChecks{
PreApply: []plancheck.PlanCheck{
plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionNoop),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ func Test_ImportBlock_InConfigDirectory(t *testing.T) {
ConfigDirectory: config.StaticDirectory(`testdata/1`),
},
{
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateVerify: true,
ConfigDirectory: config.StaticDirectory(`testdata/2`),
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportPlanVerify: true,
ConfigDirectory: config.StaticDirectory(`testdata/2`),
},
},
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ func Test_ImportBlock_InConfigFile(t *testing.T) {
ConfigFile: config.StaticFile(`testdata/1/examplecloud_container.tf`),
},
{
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateVerify: true,
ConfigFile: config.StaticFile(`testdata/2/examplecloud_container_import.tf`),
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportPlanVerify: true,
ConfigFile: config.StaticFile(`testdata/2/examplecloud_container_import.tf`),
},
},
})
Expand Down
48 changes: 48 additions & 0 deletions helper/resource/importstate/import_block_verify_plan_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package importstate_test

import (
"testing"

"github.com/hashicorp/terraform-plugin-go/tfprotov6"

"github.com/hashicorp/terraform-plugin-testing/internal/testing/testprovider"
"github.com/hashicorp/terraform-plugin-testing/internal/testing/testsdk/providerserver"
"github.com/hashicorp/terraform-plugin-testing/tfversion"

r "github.com/hashicorp/terraform-plugin-testing/helper/resource"
)

func Test_ImportBlock_VerifyPlan(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
TerraformVersionChecks: []tfversion.TerraformVersionCheck{
tfversion.SkipBelow(tfversion.Version1_5_0), // ImportBlockWithID requires Terraform 1.5.0 or later
},
ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){
"examplecloud": providerserver.NewProviderServer(testprovider.Provider{
Resources: map[string]testprovider.Resource{
"examplecloud_container": examplecloudResource(),
},
}),
},
Steps: []r.TestStep{
{
Config: `
resource "examplecloud_container" "test" {
location = "westeurope"
name = "somevalue"
}`,
},
{
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportPlanVerify: true,
},
},
})
}
37 changes: 23 additions & 14 deletions helper/resource/importstate/import_block_with_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ func Test_TestStep_ImportBlockId(t *testing.T) {
}`,
},
{
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateVerify: true,
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportPlanVerify: true,
},
},
})
Expand Down Expand Up @@ -81,11 +81,11 @@ func TestTest_TestStep_ImportBlockId_ExpectError(t *testing.T) {
location = "eastus"
name = "somevalue"
}`,
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateVerify: true,
ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op resource action, got "update" action with plan(.?)`),
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportPlanVerify: true,
ExpectError: regexp.MustCompile(`importing resource examplecloud_container.test: expected a no-op resource action, got "update" action with plan(.?)`),
},
},
})
Expand Down Expand Up @@ -120,11 +120,11 @@ func TestTest_TestStep_ImportBlockId_FailWhenPlannableImportIsNotSupported(t *te
location = "eastus"
name = "somevalue"
}`,
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportStateVerify: true,
ExpectError: regexp.MustCompile(`Terraform 1.5.0`),
ResourceName: "examplecloud_container.test",
ImportState: true,
ImportStateKind: r.ImportBlockWithID,
ImportPlanVerify: true,
ExpectError: regexp.MustCompile(`Terraform 1.5.0`),
},
},
})
Expand Down Expand Up @@ -200,6 +200,11 @@ func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore_Real_Example(t *tes
I also need to omit the `password` in the import config, otherwise the value in the config is used when importing the
with an import block and the test ends up passing regardless of whether `ImportStateVerifyIgnore` has been specified or not
*/

// In prerelease, we are choosing that ImportBlockWithID will not perform an apply, so it will not produce a new state,
// and there is no new state for ImportStateVerify to do anything meaningful with.
Comment on lines +204 to +205
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Are we planning on revisiting this decision later? If so, maybe a TODO prefix here (I know I'll forget what the original intention was of this comment 😆)

Same question for the other test below

t.Skip()

t.Parallel()

r.UnitTest(t, r.TestCase{
Expand Down Expand Up @@ -301,6 +306,10 @@ func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore_Real_Example(t *tes
}

func TestTest_TestStep_ImportBlockId_ImportStateVerifyIgnore(t *testing.T) {
// In prerelease, we are choosing that ImportBlockWithID will not perform an apply, so it will not produce a new state,
// and there is no new state for ImportStateVerify to do anything meaningful with.
t.Skip()

t.Parallel()

r.UnitTest(t, r.TestCase{
Expand Down
4 changes: 2 additions & 2 deletions helper/resource/importstate/import_command_with_id_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"github.com/hashicorp/terraform-plugin-testing/tfversion"
)

func Test_TestStep_ImportStateCheck_SkipDataSourceState(t *testing.T) {
func Test_ImportCommmandWithId_SkipDataSourceState(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
Expand Down Expand Up @@ -123,7 +123,7 @@ func Test_TestStep_ImportStateCheck_SkipDataSourceState(t *testing.T) {
})
}

func Test_TestStep_ImportStateVerify(t *testing.T) {
func Test_ImportCommandWithId_ImportStateVerify(t *testing.T) {
t.Parallel()

r.UnitTest(t, r.TestCase{
Expand Down
5 changes: 5 additions & 0 deletions helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,11 @@ type TestStep struct {
// [plancheck]: https://pkg.go.dev/github.com/hashicorp/terraform-plugin-testing/plancheck
ImportPlanChecks ImportPlanChecks

// ImportPlanVerify checks that the planned resource state, created via a plannable import
// step (ImportBlockWithID or ImportBlockWithResourceIdentity), exactly matches
// the resource state of the previous test step.
ImportPlanVerify bool

// ImportStateVerify, if true, will also check that the state values
// that are finally put into the state after import match for all the
// IDs returned by the Import. Note that this checks for strict equality
Expand Down
10 changes: 5 additions & 5 deletions helper/resource/testing_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
var statePreDestroy *terraform.State
var err error
err = runProviderCommand(ctx, t, func() error {
statePreDestroy, err = getState(ctx, t, wd)
_, statePreDestroy, err = getState(ctx, t, wd)
if err != nil {
return err
}
Expand Down Expand Up @@ -447,18 +447,18 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
}
}

func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*terraform.State, error) {
func getState(ctx context.Context, t testing.T, wd *plugintest.WorkingDir) (*tfjson.State, *terraform.State, error) {
t.Helper()

jsonState, err := wd.State(ctx)
if err != nil {
return nil, err
return nil, nil, err
}
state, err := shimStateFromJson(jsonState)
if err != nil {
t.Fatal(err)
}
return state, nil
return jsonState, state, nil
}

func stateIsEmpty(state *terraform.State) bool {
Expand Down Expand Up @@ -573,7 +573,7 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.
if err != nil {
t.Fatalf("Error running terraform refresh: %s", err)
}
state, err = getState(ctx, t, wd)
_, state, err = getState(ctx, t, wd)
if err != nil {
return err
}
Expand Down
6 changes: 3 additions & 3 deletions helper/resource/testing_new_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
}

err = runProviderCommand(ctx, t, func() error {
stateBeforeApplication, err = getState(ctx, t, wd)
_, stateBeforeApplication, err = getState(ctx, t, wd)
if err != nil {
return err
}
Expand Down Expand Up @@ -200,7 +200,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
var state *terraform.State

err := runProviderCommand(ctx, t, func() error {
state, err = getState(ctx, t, wd)
_, state, err = getState(ctx, t, wd)
if err != nil {
return err
}
Expand Down Expand Up @@ -384,7 +384,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint
var state *terraform.State

err = runProviderCommand(ctx, t, func() error {
state, err = getState(ctx, t, wd)
_, state, err = getState(ctx, t, wd)
if err != nil {
return err
}
Expand Down
84 changes: 56 additions & 28 deletions helper/resource/testing_new_import_state.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,11 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest

// get state from check sequence
var state *terraform.State
var stateJSON *tfjson.State
var err error

err = runProviderCommand(ctx, t, func() error {
state, err = getState(ctx, t, wd)
stateJSON, state, err = getState(ctx, t, wd)
if err != nil {
return err
}
Expand All @@ -72,7 +73,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
}

// Determine the ID to import
var importId string
var importId string //nolint:revive
switch {
case step.ImportStateIdFunc != nil:
logging.HelperResourceTrace(ctx, "Using TestStep ImportStateIdFunc for import identifier")
Expand Down Expand Up @@ -185,37 +186,20 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
return err
}

if plan.ResourceChanges != nil {
if len(plan.ResourceChanges) > 0 {
logging.HelperResourceDebug(ctx, fmt.Sprintf("ImportBlockWithId: %d resource changes", len(plan.ResourceChanges)))

for _, rc := range plan.ResourceChanges {
if rc.Address != resourceName {
// we're only interested in the changes for the resource being imported
continue
}
if rc.Change != nil && rc.Change.Actions != nil {
// 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
for _, action := range rc.Change.Actions {
if action != "no-op" {
var stdout string
err = runProviderCommand(ctx, t, func() error {
var err error
stdout, err = importWd.SavedPlanRawStdout(ctx)
return err
}, importWd, providers)
if err != nil {
return fmt.Errorf("retrieving formatted plan output: %w", err)
}

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)
}
}
if err := requireNoopResourceAction(ctx, t, plan, resourceName, importWd, providers); err != nil {
return err
}

if step.ImportPlanVerify {
if err := teststep.VerifyImportPlan(plan, stateJSON); err != nil {
return err
}
}
}

// TODO compare plan to state from previous step

if err := runPlanChecks(ctx, t, plan, step.ImportPlanChecks.PreApply); err != nil {
return err
}
Expand All @@ -230,7 +214,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest

var importState *terraform.State
err = runProviderCommand(ctx, t, func() error {
importState, err = getState(ctx, t, importWd)
_, importState, err = getState(ctx, t, importWd)
if err != nil {
return err
}
Expand Down Expand Up @@ -275,6 +259,10 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
identifierAttribute = "id"
}

if len(newResources) == 0 {
return fmt.Errorf("ImportStateVerify: no new resources imported")
}

for _, r := range newResources {
rIdentifier, ok := r.Primary.Attributes[identifierAttribute]

Expand Down Expand Up @@ -389,6 +377,46 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest
return nil
}

func requireNoopResourceAction(ctx context.Context, t testing.T, plan *tfjson.Plan, resourceName string, importWd *plugintest.WorkingDir, providers *providerFactories) error {
t.Helper()

rc := findResourceChangeInPlan(t, plan, resourceName)
if rc == nil || rc.Change == nil || rc.Change.Actions == nil {
// does this matter?
return nil
Comment on lines +385 to +386
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 - What this would be signifying is that there were planned changes, but they weren't to the resource we were testing. I'm actually not sure what we should do here, but I'm leaning towards throwing an error? I can't really see why you'd want/have this scenario in your test.

Something like: "expected entire plan to be no-op, found resource "" with changes", might need to refactor a little to get the affected resource name in the error here.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The other scenario this could indicate is that there were no actual changes, but we found the resource (i.e. Terraform has a bug I'd imagine?)

}

// 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
for _, action := range rc.Change.Actions {
if action != tfjson.ActionNoop {
var stdout string
err := runProviderCommand(ctx, t, func() error {
var err error
stdout, err = importWd.SavedPlanRawStdout(ctx)
return err
}, importWd, providers)
if err != nil {
return fmt.Errorf("retrieving formatted plan output: %w", err)
}

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)
}
}

return nil
}

func findResourceChangeInPlan(t testing.T, plan *tfjson.Plan, resourceName string) *tfjson.ResourceChange {
t.Helper()

for _, rc := range plan.ResourceChanges {
if rc.Address == resourceName {
return rc
}
}
return nil
}

func appendImportBlock(config string, resourceName string, importID string) string {
return config + fmt.Sprintf(``+"\n"+
`import {`+"\n"+
Expand Down
Loading
Loading