github.com/hashicorp/terraform-plugin-sdk@v1.17.2/helper/resource/testing_import_state.go (about)

     1  package resource
     2  
     3  import (
     4  	"fmt"
     5  	"log"
     6  	"reflect"
     7  	"strings"
     8  
     9  	"github.com/davecgh/go-spew/spew"
    10  	"github.com/hashicorp/hcl/v2"
    11  	"github.com/hashicorp/hcl/v2/hclsyntax"
    12  
    13  	"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
    14  	"github.com/hashicorp/terraform-plugin-sdk/internal/addrs"
    15  	"github.com/hashicorp/terraform-plugin-sdk/internal/states"
    16  	"github.com/hashicorp/terraform-plugin-sdk/terraform"
    17  )
    18  
    19  // testStepImportState runs an imort state test step
    20  func testStepImportState(
    21  	opts terraform.ContextOpts,
    22  	state *terraform.State,
    23  	step TestStep) (*terraform.State, error) {
    24  
    25  	// Determine the ID to import
    26  	var importId string
    27  	switch {
    28  	case step.ImportStateIdFunc != nil:
    29  		var err error
    30  		importId, err = step.ImportStateIdFunc(state)
    31  		if err != nil {
    32  			return state, err
    33  		}
    34  	case step.ImportStateId != "":
    35  		importId = step.ImportStateId
    36  	default:
    37  		resource, err := testResource(step, state)
    38  		if err != nil {
    39  			return state, err
    40  		}
    41  		importId = resource.Primary.ID
    42  	}
    43  
    44  	importPrefix := step.ImportStateIdPrefix
    45  	if importPrefix != "" {
    46  		importId = fmt.Sprintf("%s%s", importPrefix, importId)
    47  	}
    48  
    49  	// Setup the context. We initialize with an empty state. We use the
    50  	// full config for provider configurations.
    51  	cfg, err := testConfig(opts, step)
    52  	if err != nil {
    53  		return state, err
    54  	}
    55  
    56  	opts.Config = cfg
    57  
    58  	// import tests start with empty state
    59  	opts.State = states.NewState()
    60  
    61  	ctx, stepDiags := terraform.NewContext(&opts)
    62  	if stepDiags.HasErrors() {
    63  		return state, stepDiags.Err()
    64  	}
    65  
    66  	// The test step provides the resource address as a string, so we need
    67  	// to parse it to get an addrs.AbsResourceAddress to pass in to the
    68  	// import method.
    69  	traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(step.ResourceName), "", hcl.Pos{})
    70  	if hclDiags.HasErrors() {
    71  		return nil, hclDiags
    72  	}
    73  	importAddr, stepDiags := addrs.ParseAbsResourceInstance(traversal)
    74  	if stepDiags.HasErrors() {
    75  		return nil, stepDiags.Err()
    76  	}
    77  
    78  	// Do the import
    79  	importedState, stepDiags := ctx.Import(&terraform.ImportOpts{
    80  		// Set the module so that any provider config is loaded
    81  		Config: cfg,
    82  
    83  		Targets: []*terraform.ImportTarget{
    84  			{
    85  				Addr: importAddr,
    86  				ID:   importId,
    87  			},
    88  		},
    89  	})
    90  	if stepDiags.HasErrors() {
    91  		log.Printf("[ERROR] Test: ImportState failure: %s", stepDiags.Err())
    92  		return state, stepDiags.Err()
    93  	}
    94  
    95  	newState, err := shimNewState(importedState, step.providers)
    96  	if err != nil {
    97  		return nil, err
    98  	}
    99  
   100  	// Go through the new state and verify
   101  	if step.ImportStateCheck != nil {
   102  		var states []*terraform.InstanceState
   103  		for _, r := range newState.RootModule().Resources {
   104  			if r.Primary != nil {
   105  				is := r.Primary.DeepCopy()
   106  				is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type
   107  				states = append(states, is)
   108  			}
   109  		}
   110  		if err := step.ImportStateCheck(states); err != nil {
   111  			return state, err
   112  		}
   113  	}
   114  
   115  	// Verify that all the states match
   116  	if step.ImportStateVerify {
   117  		new := newState.RootModule().Resources
   118  		old := state.RootModule().Resources
   119  		for _, r := range new {
   120  			// Find the existing resource
   121  			var oldR *terraform.ResourceState
   122  			for _, r2 := range old {
   123  				if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type {
   124  					oldR = r2
   125  					break
   126  				}
   127  			}
   128  			if oldR == nil {
   129  				return state, fmt.Errorf(
   130  					"Failed state verification, resource with ID %s not found",
   131  					r.Primary.ID)
   132  			}
   133  
   134  			// We'll try our best to find the schema for this resource type
   135  			// so we can ignore Removed fields during validation. If we fail
   136  			// to find the schema then we won't ignore them and so the test
   137  			// will need to rely on explicit ImportStateVerifyIgnore, though
   138  			// this shouldn't happen in any reasonable case.
   139  			var rsrcSchema *schema.Resource
   140  			if providerAddr, diags := addrs.ParseAbsProviderConfigStr(r.Provider); !diags.HasErrors() {
   141  				providerType := providerAddr.ProviderConfig.Type
   142  				if provider, ok := step.providers[providerType]; ok {
   143  					if provider, ok := provider.(*schema.Provider); ok {
   144  						rsrcSchema = provider.ResourcesMap[r.Type]
   145  					}
   146  				}
   147  			}
   148  
   149  			// don't add empty flatmapped containers, so we can more easily
   150  			// compare the attributes
   151  			skipEmpty := func(k, v string) bool {
   152  				if strings.HasSuffix(k, ".#") || strings.HasSuffix(k, ".%") {
   153  					if v == "0" {
   154  						return true
   155  					}
   156  				}
   157  				return false
   158  			}
   159  
   160  			// Compare their attributes
   161  			actual := make(map[string]string)
   162  			for k, v := range r.Primary.Attributes {
   163  				if skipEmpty(k, v) {
   164  					continue
   165  				}
   166  				actual[k] = v
   167  			}
   168  
   169  			expected := make(map[string]string)
   170  			for k, v := range oldR.Primary.Attributes {
   171  				if skipEmpty(k, v) {
   172  					continue
   173  				}
   174  				expected[k] = v
   175  			}
   176  
   177  			// Remove fields we're ignoring
   178  			for _, v := range step.ImportStateVerifyIgnore {
   179  				for k := range actual {
   180  					if strings.HasPrefix(k, v) {
   181  						delete(actual, k)
   182  					}
   183  				}
   184  				for k := range expected {
   185  					if strings.HasPrefix(k, v) {
   186  						delete(expected, k)
   187  					}
   188  				}
   189  			}
   190  
   191  			// Also remove any attributes that are marked as "Removed" in the
   192  			// schema, if we have a schema to check that against.
   193  			if rsrcSchema != nil {
   194  				for k := range actual {
   195  					for _, schema := range rsrcSchema.SchemasForFlatmapPath(k) {
   196  						if schema.Removed != "" {
   197  							delete(actual, k)
   198  							break
   199  						}
   200  					}
   201  				}
   202  				for k := range expected {
   203  					for _, schema := range rsrcSchema.SchemasForFlatmapPath(k) {
   204  						if schema.Removed != "" {
   205  							delete(expected, k)
   206  							break
   207  						}
   208  					}
   209  				}
   210  			}
   211  
   212  			if !reflect.DeepEqual(actual, expected) {
   213  				// Determine only the different attributes
   214  				for k, v := range expected {
   215  					if av, ok := actual[k]; ok && v == av {
   216  						delete(expected, k)
   217  						delete(actual, k)
   218  					}
   219  				}
   220  
   221  				spewConf := spew.NewDefaultConfig()
   222  				spewConf.SortKeys = true
   223  				return state, fmt.Errorf(
   224  					"ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+
   225  						"\n\n%s\n\n%s",
   226  					spewConf.Sdump(actual), spewConf.Sdump(expected))
   227  			}
   228  		}
   229  	}
   230  
   231  	// Return the old state (non-imported) so we don't change anything.
   232  	return state, nil
   233  }