2023-07-03 20:21:30 +00:00
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0
2022-08-06 14:21:18 +00:00
package resource
import (
"context"
"fmt"
"reflect"
"strings"
2023-03-20 20:25:53 +00:00
"github.com/google/go-cmp/cmp"
2022-12-24 16:57:19 +00:00
"github.com/mitchellh/go-testing-interface"
2022-08-06 14:21:18 +00:00
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging"
"github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
func testStepNewImportState ( ctx context . Context , t testing . T , helper * plugintest . Helper , wd * plugintest . WorkingDir , step TestStep , cfg string , providers * providerFactories ) error {
t . Helper ( )
if step . ResourceName == "" {
t . Fatal ( "ResourceName is required for an import state test" )
}
// get state from check sequence
var state * terraform . State
var err error
err = runProviderCommand ( ctx , t , func ( ) error {
state , err = getState ( ctx , t , wd )
if err != nil {
return err
}
return nil
} , wd , providers )
if err != nil {
t . Fatalf ( "Error getting state: %s" , err )
}
// Determine the ID to import
var importId string
switch {
case step . ImportStateIdFunc != nil :
logging . HelperResourceTrace ( ctx , "Using TestStep ImportStateIdFunc for import identifier" )
var err error
logging . HelperResourceDebug ( ctx , "Calling TestStep ImportStateIdFunc" )
importId , err = step . ImportStateIdFunc ( state )
if err != nil {
t . Fatal ( err )
}
logging . HelperResourceDebug ( ctx , "Called TestStep ImportStateIdFunc" )
case step . ImportStateId != "" :
logging . HelperResourceTrace ( ctx , "Using TestStep ImportStateId for import identifier" )
importId = step . ImportStateId
default :
logging . HelperResourceTrace ( ctx , "Using resource identifier for import identifier" )
resource , err := testResource ( step , state )
if err != nil {
t . Fatal ( err )
}
importId = resource . Primary . ID
}
if step . ImportStateIdPrefix != "" {
logging . HelperResourceTrace ( ctx , "Prepending TestStep ImportStateIdPrefix for import identifier" )
importId = step . ImportStateIdPrefix + importId
}
logging . HelperResourceTrace ( ctx , fmt . Sprintf ( "Using import identifier: %s" , importId ) )
// Create working directory for import tests
if step . Config == "" {
logging . HelperResourceTrace ( ctx , "Using prior TestStep Config for import" )
step . Config = cfg
if step . Config == "" {
t . Fatal ( "Cannot import state with no specified config" )
}
}
2022-12-24 16:57:19 +00:00
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 ( )
}
2022-08-06 14:21:18 +00:00
err = importWd . SetConfig ( ctx , step . Config )
if err != nil {
t . Fatalf ( "Error setting test config: %s" , err )
}
logging . HelperResourceDebug ( ctx , "Running Terraform CLI init and import" )
2022-12-24 16:57:19 +00:00
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 )
}
2022-08-06 14:21:18 +00:00
}
err = runProviderCommand ( ctx , t , func ( ) error {
return importWd . Import ( ctx , step . ResourceName , importId )
} , importWd , providers )
if err != nil {
return err
}
var importState * terraform . State
err = runProviderCommand ( ctx , t , func ( ) error {
importState , err = getState ( ctx , t , importWd )
if err != nil {
return err
}
return nil
} , importWd , providers )
if err != nil {
t . Fatalf ( "Error getting state: %s" , err )
}
// Go through the imported state and verify
if step . ImportStateCheck != nil {
logging . HelperResourceTrace ( ctx , "Using TestStep ImportStateCheck" )
var states [ ] * terraform . InstanceState
2022-12-24 16:57:19 +00:00
for address , r := range importState . RootModule ( ) . Resources {
if strings . HasPrefix ( address , "data." ) {
continue
}
if r . Primary == nil {
continue
2022-08-06 14:21:18 +00:00
}
2022-12-24 16:57:19 +00:00
is := r . Primary . DeepCopy ( )
is . Ephemeral . Type = r . Type // otherwise the check function cannot see the type
states = append ( states , is )
2022-08-06 14:21:18 +00:00
}
logging . HelperResourceDebug ( ctx , "Calling TestStep ImportStateCheck" )
if err := step . ImportStateCheck ( states ) ; err != nil {
t . Fatal ( err )
}
logging . HelperResourceDebug ( ctx , "Called TestStep ImportStateCheck" )
}
// Verify that all the states match
if step . ImportStateVerify {
logging . HelperResourceTrace ( ctx , "Using TestStep ImportStateVerify" )
2022-12-24 16:57:19 +00:00
// Ensure that we do not match against data sources as they
// cannot be imported and are not what we want to verify.
// Mode is not present in ResourceState so we use the
// stringified ResourceStateKey for comparison.
newResources := make ( map [ string ] * terraform . ResourceState )
for k , v := range importState . RootModule ( ) . Resources {
if ! strings . HasPrefix ( k , "data." ) {
newResources [ k ] = v
}
}
oldResources := make ( map [ string ] * terraform . ResourceState )
for k , v := range state . RootModule ( ) . Resources {
if ! strings . HasPrefix ( k , "data." ) {
oldResources [ k ] = v
}
}
2022-08-06 14:21:18 +00:00
for _ , r := range newResources {
// Find the existing resource
var oldR * terraform . ResourceState
2022-12-24 16:57:19 +00:00
for _ , r2 := range oldResources {
2022-08-06 14:21:18 +00:00
if r2 . Primary != nil && r2 . Primary . ID == r . Primary . ID && r2 . Type == r . Type && r2 . Provider == r . Provider {
oldR = r2
break
}
}
if oldR == nil || oldR . Primary == nil {
t . Fatalf (
"Failed state verification, resource with ID %s not found" ,
r . Primary . ID )
}
// don't add empty flatmapped containers, so we can more easily
// compare the attributes
skipEmpty := func ( k , v string ) bool {
if strings . HasSuffix ( k , ".#" ) || strings . HasSuffix ( k , ".%" ) {
if v == "0" {
return true
}
}
return false
}
// Compare their attributes
actual := make ( map [ string ] string )
for k , v := range r . Primary . Attributes {
if skipEmpty ( k , v ) {
continue
}
actual [ k ] = v
}
expected := make ( map [ string ] string )
for k , v := range oldR . Primary . Attributes {
if skipEmpty ( k , v ) {
continue
}
expected [ k ] = v
}
// Remove fields we're ignoring
for _ , v := range step . ImportStateVerifyIgnore {
for k := range actual {
if strings . HasPrefix ( k , v ) {
delete ( actual , k )
}
}
for k := range expected {
if strings . HasPrefix ( k , v ) {
delete ( expected , k )
}
}
}
// timeouts are only _sometimes_ added to state. To
// account for this, just don't compare timeouts at
// all.
for k := range actual {
if strings . HasPrefix ( k , "timeouts." ) {
delete ( actual , k )
}
if k == "timeouts" {
delete ( actual , k )
}
}
for k := range expected {
if strings . HasPrefix ( k , "timeouts." ) {
delete ( expected , k )
}
if k == "timeouts" {
delete ( expected , k )
}
}
if ! reflect . DeepEqual ( actual , expected ) {
// Determine only the different attributes
2023-03-20 20:25:53 +00:00
// go-cmp tries to show surrounding identical map key/value for
// context of differences, which may be confusing.
2022-08-06 14:21:18 +00:00
for k , v := range expected {
if av , ok := actual [ k ] ; ok && v == av {
delete ( expected , k )
delete ( actual , k )
}
}
2023-03-20 20:25:53 +00:00
if diff := cmp . Diff ( expected , actual ) ; diff != "" {
return fmt . Errorf ( "ImportStateVerify attributes not equivalent. Difference is shown below. The - symbol indicates attributes missing after import.\n\n%s" , diff )
}
2022-08-06 14:21:18 +00:00
}
}
}
return nil
}