diff --git a/.changes/unreleased/BUG FIXES-20250602-073221.yaml b/.changes/unreleased/BUG FIXES-20250602-073221.yaml new file mode 100644 index 000000000..2b758fa6e --- /dev/null +++ b/.changes/unreleased/BUG FIXES-20250602-073221.yaml @@ -0,0 +1,5 @@ +kind: BUG FIXES +body: 'helper/resource: Updated `ImportBlockWith*` import state modes to use the `ExpectNonEmpty` field to allow non-empty import plans to pass successfully.' +time: 2025-06-02T07:32:21.384247-04:00 +custom: + Issue: "518" diff --git a/helper/resource/importstate/import_block_with_id_test.go b/helper/resource/importstate/import_block_with_id_test.go index c11754a6d..b401deef5 100644 --- a/helper/resource/importstate/import_block_with_id_test.go +++ b/helper/resource/importstate/import_block_with_id_test.go @@ -99,6 +99,58 @@ func TestImportBlock_WithID_ExpectError(t *testing.T) { }) } +func TestImportBlock_WithID_ExpectNonEmptyPlan(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" + }`, + }, + { + Config: ` + resource "examplecloud_container" "test" { + location = "eastus" + name = "somevalue" + } + + import { + to = examplecloud_container.test + id = "westeurope/somevalue" + } + `, + ResourceName: "examplecloud_container.test", + ImportState: true, + ImportStateKind: r.ImportBlockWithID, + ImportStateConfigExact: true, + ExpectNonEmptyPlan: true, + ImportPlanChecks: r.ImportPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction("examplecloud_container.test", plancheck.ResourceActionUpdate), + // The location address is imported as "westeurope/somevalue", which will be updated by the config to "eastus" + plancheck.ExpectKnownValue("examplecloud_container.test", tfjsonpath.New("location"), knownvalue.StringExact("eastus")), + plancheck.ExpectUnknownValue("examplecloud_container.test", tfjsonpath.New("id")), + }, + }, + }, + }, + }) +} + func TestImportBlock_WithID_FailWhenNotSupported(t *testing.T) { t.Parallel() diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 9e7192e4e..c3dd3bdc9 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -674,6 +674,11 @@ type TestStep struct { // ID of that resource. ImportState bool + // ImportStateKind controls the method of import that is used in combination with the other import-related fields on the TestStep struct. + // + // - By default, ImportCommandWithID is used, which tests import by using the ID string with the `terraform import` command. This was the original behavior prior to introducing the ImportStateKind field. + // - ImportBlockWithID tests import by using the ID string in an import configuration block with the `terraform plan` command. + // - ImportBlockWithResourceIdentity imports the state using an import configuration block with a resource identity. ImportStateKind ImportStateKind // ImportStateId is the ID to perform an ImportState operation with. diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index f2e265d56..82a10483e 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -221,8 +221,9 @@ func testImportBlock(ctx context.Context, t testing.T, workingDir *plugintest.Wo switch { case importing == nil: return fmt.Errorf("importing resource %s: expected an import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) - - case !actions.NoOp(): + // By default we want to ensure there isn't a proposed plan after importing, but for some resources this is unavoidable. + // An example would be importing a resource that cannot read it's entire value back from the remote API. + case !step.ExpectNonEmptyPlan && !actions.NoOp(): return fmt.Errorf("importing resource %s: expected a no-op import operation, got %q action with plan \nstdout:\n\n%s", resourceChangeUnderTest.Address, actions, savedPlanRawStdout(ctx, t, workingDir, providers)) }