diff --git a/.changelog/1052.txt b/.changelog/1052.txt new file mode 100644 index 0000000000..a54e1e9b51 --- /dev/null +++ b/.changelog/1052.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +helper/resource: Add ImportStatePersist to optionally persist state generated during import +``` diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 53c3746d84..e509586e35 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -564,6 +564,12 @@ type TestStep struct { ImportStateVerify bool ImportStateVerifyIgnore []string + // ImportStatePersist, if true, will update the persisted state with the + // state generated by the import operation (i.e., terraform import). When + // false (default) the state generated by the import operation is discarded + // at the end of the test step that is verifying import behavior. + ImportStatePersist bool + // ProviderFactories can be specified for the providers that are valid for // this TestStep. When providers are specified at the TestStep level, all // TestStep within a TestCase must declare providers. diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index ec61b055f3..fc4ebc9cb0 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -7,7 +7,7 @@ import ( "strings" "github.com/davecgh/go-spew/spew" - testing "github.com/mitchellh/go-testing-interface" + "github.com/mitchellh/go-testing-interface" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" @@ -86,8 +86,17 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest t.Fatal("Cannot import state with no specified config") } } - importWd := helper.RequireNewWorkingDir(ctx, t) - defer importWd.Close() + + var importWd *plugintest.WorkingDir + + // Use the same working directory to persist the state from import + if step.ImportStatePersist { + importWd = wd + } else { + importWd = helper.RequireNewWorkingDir(ctx, t) + defer importWd.Close() + } + err = importWd.SetConfig(ctx, step.Config) if err != nil { t.Fatalf("Error setting test config: %s", err) @@ -95,11 +104,13 @@ func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest logging.HelperResourceDebug(ctx, "Running Terraform CLI init and import") - err = runProviderCommand(ctx, t, func() error { - return importWd.Init(ctx) - }, importWd, providers) - if err != nil { - t.Fatalf("Error running init: %s", err) + if !step.ImportStatePersist { + err = runProviderCommand(ctx, t, func() error { + return importWd.Init(ctx) + }, importWd, providers) + if err != nil { + t.Fatalf("Error running init: %s", err) + } } err = runProviderCommand(ctx, t, func() error { diff --git a/helper/resource/teststep_providers_test.go b/helper/resource/teststep_providers_test.go index f12b4dc323..6f4b01e49f 100644 --- a/helper/resource/teststep_providers_test.go +++ b/helper/resource/teststep_providers_test.go @@ -569,3 +569,426 @@ func TestTest_TestStep_ProviderFactories_To_ExternalProviders(t *testing.T) { }, }) } + +func TestTest_TestStep_ProviderFactories_Import_Inline(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "length": { + Required: true, + ForceNew: true, + Type: schema.TypeInt, + }, + "result": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + val := d.Id() + + d.SetId("none") + + err := d.Set("result", val) + if err != nil { + panic(err) + } + + err = d.Set("length", len(val)) + if err != nil { + panic(err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + }, + }, + }, nil + }, + }, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testCheckResourceAttrInstanceState("id", "none"), + testCheckResourceAttrInstanceState("result", "Z=:cbrJE?Ltg"), + testCheckResourceAttrInstanceState("length", "12"), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_Inline_WithPersistMatch(t *testing.T) { + var result1, result2 string + + t.Parallel() + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "length": { + Required: true, + ForceNew: true, + Type: schema.TypeInt, + }, + "result": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + val := d.Id() + + d.SetId("none") + + err := d.Set("result", val) + if err != nil { + panic(err) + } + + err = d.Set("length", len(val)) + if err != nil { + panic(err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + }, + }, + }, nil + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_Inline_WithoutPersist(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "random": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "random_password": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("none") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "length": { + Required: true, + ForceNew: true, + Type: schema.TypeInt, + }, + "result": { + Type: schema.TypeString, + Computed: true, + Sensitive: true, + }, + + "id": { + Computed: true, + Type: schema.TypeString, + }, + }, + Importer: &schema.ResourceImporter{ + StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + val := d.Id() + + d.SetId("none") + + err := d.Set("result", val) + if err != nil { + panic(err) + } + + err = d.Set("length", len(val)) + if err != nil { + panic(err) + } + + return []*schema.ResourceData{d}, nil + }, + }, + }, + }, + }, nil + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: false, + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + TestCheckNoResourceAttr("random_password.test", "result"), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_External(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testCheckResourceAttrInstanceState("id", "none"), + testCheckResourceAttrInstanceState("result", "Z=:cbrJE?Ltg"), + testCheckResourceAttrInstanceState("length", "12"), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_External_WithPersistMatch(t *testing.T) { + var result1, result2 string + + t.Parallel() + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: true, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesEqual(&result1, &result2), + ), + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Import_External_WithoutPersistNonMatch(t *testing.T) { + var result1, result2 string + + t.Parallel() + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + Steps: []TestStep{ + { + Config: `resource "random_password" "test" { length = 12 }`, + ResourceName: "random_password.test", + ImportState: true, + ImportStateId: "Z=:cbrJE?Ltg", + ImportStatePersist: false, + ImportStateCheck: composeImportStateCheck( + testExtractResourceAttrInstanceState("result", &result1), + ), + }, + { + Config: `resource "random_password" "test" { length = 12 }`, + Check: ComposeTestCheckFunc( + testExtractResourceAttr("random_password.test", "result", &result2), + testCheckAttributeValuesDiffer(&result1, &result2), + ), + }, + }, + }) +} + +func composeImportStateCheck(fs ...ImportStateCheckFunc) ImportStateCheckFunc { + return func(s []*terraform.InstanceState) error { + for i, f := range fs { + if err := f(s); err != nil { + return fmt.Errorf("check %d/%d error: %s", i+1, len(fs), err) + } + } + + return nil + } +} + +func testExtractResourceAttrInstanceState(attributeName string, attributeValue *string) ImportStateCheckFunc { + return func(is []*terraform.InstanceState) error { + if len(is) != 1 { + return fmt.Errorf("unexpected number of instance states: %d", len(is)) + } + + s := is[0] + + attrValue, ok := s.Attributes[attributeName] + if !ok { + return fmt.Errorf("attribute %s not found in instance state", attributeName) + } + + *attributeValue = attrValue + + return nil + } +} + +func testCheckResourceAttrInstanceState(attributeName, attributeValue string) ImportStateCheckFunc { + return func(is []*terraform.InstanceState) error { + if len(is) != 1 { + return fmt.Errorf("unexpected number of instance states: %d", len(is)) + } + + s := is[0] + + attrVal, ok := s.Attributes[attributeName] + if !ok { + return fmt.Errorf("attribute %s found in instance state", attributeName) + } + + if attrVal != attributeValue { + return fmt.Errorf("expected: %s got: %s", attributeValue, attrVal) + } + + return nil + } +} + +func testExtractResourceAttr(resourceName string, attributeName string, attributeValue *string) TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[resourceName] + + if !ok { + return fmt.Errorf("resource name %s not found in state", resourceName) + } + + attrValue, ok := rs.Primary.Attributes[attributeName] + + if !ok { + return fmt.Errorf("attribute %s not found in resource %s state", attributeName, resourceName) + } + + *attributeValue = attrValue + + return nil + } +} + +func testCheckAttributeValuesEqual(i *string, j *string) TestCheckFunc { + return func(s *terraform.State) error { + if testStringValue(i) != testStringValue(j) { + return fmt.Errorf("attribute values are different, got %s and %s", testStringValue(i), testStringValue(j)) + } + + return nil + } +} + +func testCheckAttributeValuesDiffer(i *string, j *string) TestCheckFunc { + return func(s *terraform.State) error { + if testStringValue(i) == testStringValue(j) { + return fmt.Errorf("attribute values are the same") + } + + return nil + } +} + +func testStringValue(sPtr *string) string { + if sPtr == nil { + return "" + } + + return *sPtr +}