Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TestStep.ImportBlock: testing for import blocks #141

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions helper/resource/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -560,12 +560,25 @@ type TestStep struct {
//---------------------------------------------------------------
// ImportState testing
//---------------------------------------------------------------
// Terraform has two workflows for importing resources: the import CLI
// command, which writes directly to state, and the import block in HCL,
// which imports to state via the normal plan and apply workflow.

// ImportState, if true, will test the functionality of ImportState
// by importing the resource with ResourceName (must be set) and the
// ID of that resource.
// By default, the "terraform import" command will be run. To test import
// block functionality instead, set ImportBlock to true.
ImportState bool

// ImportBlock, if true, enables a sub-mode of ImportState testing. In this
// mode, an import block is added to the config, and plan and apply are run.
ImportBlock bool

// ImportBlockConfig is an optional string with the import block
// configuration to use when ImportBlock is true.
ImportBlockConfig string

// ImportStateId is the ID to perform an ImportState operation with.
// This is optional. If it isn't set, then the resource ID is automatically
// determined by inspecting the state for ResourceName's ID.
Expand Down
7 changes: 6 additions & 1 deletion helper/resource/testing_new.go
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,12 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest
if step.ImportState {
logging.HelperResourceTrace(ctx, "TestStep is ImportState mode")

err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers)
var err error
if step.ImportBlock {
err = testStepNewImportBlock(ctx, t, helper, wd, step, appliedCfg, providers)
} else {
err = testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers)
}
if step.ExpectError != nil {
logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError")
if err == nil {
Expand Down
305 changes: 305 additions & 0 deletions helper/resource/testing_new_import_block.go
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 an import block, then plans and applies the import.
// Optionally attempts to generate resource 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
}
Loading