-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add a new sub-mode of TestStep.ImportState for ImportBlock testing.
- Loading branch information
Showing
4 changed files
with
608 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,305 @@ | ||
// Copyright (c) HashiCorp, Inc. | ||
// SPDX-License-Identifier: MPL-2.0 | ||
|
||
package resource | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
"reflect" | ||
"strings" | ||
|
||
"github.com/google/go-cmp/cmp" | ||
"github.com/mitchellh/go-testing-interface" | ||
|
||
"github.com/hashicorp/terraform-plugin-testing/terraform" | ||
|
||
"github.com/hashicorp/terraform-plugin-testing/internal/logging" | ||
"github.com/hashicorp/terraform-plugin-testing/internal/plugintest" | ||
) | ||
|
||
// Generates a config with import block, then plans and applies the import. | ||
// Optionally attempts to generate configuration during the plan step. | ||
func testStepNewImportBlock(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") | ||
} | ||
} | ||
|
||
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() | ||
} | ||
|
||
var importBlockConfig string | ||
|
||
if step.ImportBlockConfig != "" { | ||
importBlockConfig = step.ImportBlockConfig + "\n" | ||
} else { | ||
importBlockConfig = fmt.Sprintf(`import { | ||
to = %s | ||
id = "%s" | ||
} | ||
`, step.ResourceName, importId) | ||
} | ||
|
||
err = importWd.SetConfig(ctx, importBlockConfig+step.Config) | ||
if err != nil { | ||
t.Fatalf("Error setting test config: %s", err) | ||
} | ||
|
||
logging.HelperResourceDebug(ctx, "Running Terraform CLI init and plan") | ||
|
||
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 { | ||
return importWd.CreatePlan(ctx) | ||
}, importWd, providers) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
logging.HelperResourceDebug(ctx, "Running Terraform CLI apply") | ||
|
||
err = runProviderCommand(ctx, t, func() error { | ||
return importWd.Apply(ctx) | ||
}, 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 | ||
for address, r := range importState.RootModule().Resources { | ||
if strings.HasPrefix(address, "data.") { | ||
continue | ||
} | ||
|
||
if r.Primary == nil { | ||
continue | ||
} | ||
|
||
is := r.Primary.DeepCopy() | ||
is.Ephemeral.Type = r.Type // otherwise the check function cannot see the type | ||
states = append(states, is) | ||
} | ||
|
||
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") | ||
|
||
// 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 | ||
} | ||
} | ||
|
||
for _, r := range newResources { | ||
// Find the existing resource | ||
var oldR *terraform.ResourceState | ||
for _, r2 := range oldResources { | ||
|
||
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 | ||
// go-cmp tries to show surrounding identical map key/value for | ||
// context of differences, which may be confusing. | ||
for k, v := range expected { | ||
if av, ok := actual[k]; ok && v == av { | ||
delete(expected, k) | ||
delete(actual, k) | ||
} | ||
} | ||
|
||
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) | ||
} | ||
} | ||
} | ||
} | ||
|
||
return nil | ||
} |
Oops, something went wrong.