mirror of
https://github.com/opentofu/opentofu.git
synced 2025-01-16 03:32:54 -06:00
bd1a215580
Due to the lossiness of our legacy models for diff and state, shimming a diff and then creating a state from it produces a different result than shimming a state directly. That means that ImportStateVerify no longer works as expected if there are any Computed attributes in the schema where d.Set isn't called during Read. Fixing that for every case would require some risky changes to the shim behavior, so we're instead going to ask provider developers to address it by adding `d.Set` calls where needed, since that is the contract for "Computed" anyway -- a default value should be produced during Create, and thus by extension during Import. However, since a common situation where this occurs is attributes marked as "Removed", since all of the code that deals with them has generally been deleted, we'll avoid problems in that case here by treating Removed attributes as ignored for the purposes of ImportStateVerify. This required exporting some functionality that was formerly unexported in helper/schema, but it's a relatively harmless schema introspection function so shouldn't be a big deal to export it.
234 lines
6.0 KiB
Go
234 lines
6.0 KiB
Go
package resource
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"reflect"
|
|
"strings"
|
|
|
|
"github.com/davecgh/go-spew/spew"
|
|
"github.com/hashicorp/hcl2/hcl"
|
|
"github.com/hashicorp/hcl2/hcl/hclsyntax"
|
|
|
|
"github.com/hashicorp/terraform/addrs"
|
|
"github.com/hashicorp/terraform/helper/schema"
|
|
"github.com/hashicorp/terraform/states"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
// testStepImportState runs an imort state test step
|
|
func testStepImportState(
|
|
opts terraform.ContextOpts,
|
|
state *terraform.State,
|
|
step TestStep) (*terraform.State, error) {
|
|
|
|
// Determine the ID to import
|
|
var importId string
|
|
switch {
|
|
case step.ImportStateIdFunc != nil:
|
|
var err error
|
|
importId, err = step.ImportStateIdFunc(state)
|
|
if err != nil {
|
|
return state, err
|
|
}
|
|
case step.ImportStateId != "":
|
|
importId = step.ImportStateId
|
|
default:
|
|
resource, err := testResource(step, state)
|
|
if err != nil {
|
|
return state, err
|
|
}
|
|
importId = resource.Primary.ID
|
|
}
|
|
|
|
importPrefix := step.ImportStateIdPrefix
|
|
if importPrefix != "" {
|
|
importId = fmt.Sprintf("%s%s", importPrefix, importId)
|
|
}
|
|
|
|
// Setup the context. We initialize with an empty state. We use the
|
|
// full config for provider configurations.
|
|
cfg, err := testConfig(opts, step)
|
|
if err != nil {
|
|
return state, err
|
|
}
|
|
|
|
opts.Config = cfg
|
|
|
|
// import tests start with empty state
|
|
opts.State = states.NewState()
|
|
|
|
ctx, stepDiags := terraform.NewContext(&opts)
|
|
if stepDiags.HasErrors() {
|
|
return state, stepDiags.Err()
|
|
}
|
|
|
|
// The test step provides the resource address as a string, so we need
|
|
// to parse it to get an addrs.AbsResourceAddress to pass in to the
|
|
// import method.
|
|
traversal, hclDiags := hclsyntax.ParseTraversalAbs([]byte(step.ResourceName), "", hcl.Pos{})
|
|
if hclDiags.HasErrors() {
|
|
return nil, hclDiags
|
|
}
|
|
importAddr, stepDiags := addrs.ParseAbsResourceInstance(traversal)
|
|
if stepDiags.HasErrors() {
|
|
return nil, stepDiags.Err()
|
|
}
|
|
|
|
// Do the import
|
|
importedState, stepDiags := ctx.Import(&terraform.ImportOpts{
|
|
// Set the module so that any provider config is loaded
|
|
Config: cfg,
|
|
|
|
Targets: []*terraform.ImportTarget{
|
|
&terraform.ImportTarget{
|
|
Addr: importAddr,
|
|
ID: importId,
|
|
},
|
|
},
|
|
})
|
|
if stepDiags.HasErrors() {
|
|
log.Printf("[ERROR] Test: ImportState failure: %s", stepDiags.Err())
|
|
return state, stepDiags.Err()
|
|
}
|
|
|
|
newState, err := shimNewState(importedState, step.providers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Go through the new state and verify
|
|
if step.ImportStateCheck != nil {
|
|
var states []*terraform.InstanceState
|
|
for _, r := range newState.RootModule().Resources {
|
|
if r.Primary != nil {
|
|
is := r.Primary.DeepCopy()
|
|
is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type
|
|
states = append(states, is)
|
|
}
|
|
}
|
|
if err := step.ImportStateCheck(states); err != nil {
|
|
return state, err
|
|
}
|
|
}
|
|
|
|
// Verify that all the states match
|
|
if step.ImportStateVerify {
|
|
new := newState.RootModule().Resources
|
|
old := state.RootModule().Resources
|
|
for _, r := range new {
|
|
// Find the existing resource
|
|
var oldR *terraform.ResourceState
|
|
for _, r2 := range old {
|
|
if r2.Primary != nil && r2.Primary.ID == r.Primary.ID && r2.Type == r.Type {
|
|
oldR = r2
|
|
break
|
|
}
|
|
}
|
|
if oldR == nil {
|
|
return state, fmt.Errorf(
|
|
"Failed state verification, resource with ID %s not found",
|
|
r.Primary.ID)
|
|
}
|
|
|
|
// We'll try our best to find the schema for this resource type
|
|
// so we can ignore Removed fields during validation. If we fail
|
|
// to find the schema then we won't ignore them and so the test
|
|
// will need to rely on explicit ImportStateVerifyIgnore, though
|
|
// this shouldn't happen in any reasonable case.
|
|
var rsrcSchema *schema.Resource
|
|
if providerAddr, diags := addrs.ParseAbsProviderConfigStr(r.Provider); !diags.HasErrors() {
|
|
providerType := providerAddr.ProviderConfig.Type
|
|
if provider, ok := step.providers[providerType]; ok {
|
|
if provider, ok := provider.(*schema.Provider); ok {
|
|
rsrcSchema = provider.ResourcesMap[r.Type]
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Also remove any attributes that are marked as "Removed" in the
|
|
// schema, if we have a schema to check that against.
|
|
if rsrcSchema != nil {
|
|
for k := range actual {
|
|
for _, schema := range rsrcSchema.SchemasForFlatmapPath(k) {
|
|
if schema.Removed != "" {
|
|
delete(actual, k)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
for k := range expected {
|
|
for _, schema := range rsrcSchema.SchemasForFlatmapPath(k) {
|
|
if schema.Removed != "" {
|
|
delete(expected, k)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !reflect.DeepEqual(actual, expected) {
|
|
// Determine only the different attributes
|
|
for k, v := range expected {
|
|
if av, ok := actual[k]; ok && v == av {
|
|
delete(expected, k)
|
|
delete(actual, k)
|
|
}
|
|
}
|
|
|
|
spewConf := spew.NewDefaultConfig()
|
|
spewConf.SortKeys = true
|
|
return state, fmt.Errorf(
|
|
"ImportStateVerify attributes not equivalent. Difference is shown below. Top is actual, bottom is expected."+
|
|
"\n\n%s\n\n%s",
|
|
spewConf.Sdump(actual), spewConf.Sdump(expected))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return the old state (non-imported) so we don't change anything.
|
|
return state, nil
|
|
}
|