mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-03-25 14:00:21 -04:00
This adds the Taint field to the acceptance testing framework, allowing
the ability to pre-taint resources at the beginning of a particular
TestStep. This can be useful for when an explicit ForceNew is required
for a specific resource for troubleshooting things like diff mismatches,
etc.
The field accepts resource addresses as a list of strings. To keep
things simple for the time being, only addresses in the root module are
accepted. If we ever want to expand this past that, I'd be almost
inclined to add some facilities to the core terraform package to help
translate actual module resource addresses (ie:
module.foo.module.bar.some_resource.baz) into the correct state, versus
the current convention in some acceptance testing facilities that take
the module address as a list of strings (ie: []string{"root", "foo",
"bar"}).
182 lines
4.6 KiB
Go
182 lines
4.6 KiB
Go
package resource
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log"
|
|
"strings"
|
|
|
|
"github.com/hashicorp/errwrap"
|
|
"github.com/hashicorp/terraform/terraform"
|
|
)
|
|
|
|
// testStepConfig runs a config-mode test step
|
|
func testStepConfig(
|
|
opts terraform.ContextOpts,
|
|
state *terraform.State,
|
|
step TestStep) (*terraform.State, error) {
|
|
return testStep(opts, state, step)
|
|
}
|
|
|
|
func testStep(
|
|
opts terraform.ContextOpts,
|
|
state *terraform.State,
|
|
step TestStep) (*terraform.State, error) {
|
|
// Pre-taint any resources that have been defined in Taint, as long as this
|
|
// is not a destroy step.
|
|
if !step.Destroy {
|
|
if err := testStepTaint(state, step); err != nil {
|
|
return state, err
|
|
}
|
|
}
|
|
|
|
mod, err := testModule(opts, step)
|
|
if err != nil {
|
|
return state, err
|
|
}
|
|
|
|
// Build the context
|
|
opts.Module = mod
|
|
opts.State = state
|
|
opts.Destroy = step.Destroy
|
|
ctx, err := terraform.NewContext(&opts)
|
|
if err != nil {
|
|
return state, fmt.Errorf("Error initializing context: %s", err)
|
|
}
|
|
if diags := ctx.Validate(); len(diags) > 0 {
|
|
if diags.HasErrors() {
|
|
return nil, errwrap.Wrapf("config is invalid: {{err}}", diags.Err())
|
|
}
|
|
|
|
log.Printf("[WARN] Config warnings:\n%s", diags)
|
|
}
|
|
|
|
// Refresh!
|
|
state, err = ctx.Refresh()
|
|
if err != nil {
|
|
return state, fmt.Errorf(
|
|
"Error refreshing: %s", err)
|
|
}
|
|
|
|
// If this step is a PlanOnly step, skip over this first Plan and subsequent
|
|
// Apply, and use the follow up Plan that checks for perpetual diffs
|
|
if !step.PlanOnly {
|
|
// Plan!
|
|
if p, err := ctx.Plan(); err != nil {
|
|
return state, fmt.Errorf(
|
|
"Error planning: %s", err)
|
|
} else {
|
|
log.Printf("[WARN] Test: Step plan: %s", p)
|
|
}
|
|
|
|
// We need to keep a copy of the state prior to destroying
|
|
// such that destroy steps can verify their behaviour in the check
|
|
// function
|
|
stateBeforeApplication := state.DeepCopy()
|
|
|
|
// Apply!
|
|
state, err = ctx.Apply()
|
|
if err != nil {
|
|
return state, fmt.Errorf("Error applying: %s", err)
|
|
}
|
|
|
|
// Check! Excitement!
|
|
if step.Check != nil {
|
|
if step.Destroy {
|
|
if err := step.Check(stateBeforeApplication); err != nil {
|
|
return state, fmt.Errorf("Check failed: %s", err)
|
|
}
|
|
} else {
|
|
if err := step.Check(state); err != nil {
|
|
return state, fmt.Errorf("Check failed: %s", err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Now, verify that Plan is now empty and we don't have a perpetual diff issue
|
|
// We do this with TWO plans. One without a refresh.
|
|
var p *terraform.Plan
|
|
if p, err = ctx.Plan(); err != nil {
|
|
return state, fmt.Errorf("Error on follow-up plan: %s", err)
|
|
}
|
|
if p.Diff != nil && !p.Diff.Empty() {
|
|
if step.ExpectNonEmptyPlan {
|
|
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
|
} else {
|
|
return state, fmt.Errorf(
|
|
"After applying this step, the plan was not empty:\n\n%s", p)
|
|
}
|
|
}
|
|
|
|
// And another after a Refresh.
|
|
if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) {
|
|
state, err = ctx.Refresh()
|
|
if err != nil {
|
|
return state, fmt.Errorf(
|
|
"Error on follow-up refresh: %s", err)
|
|
}
|
|
}
|
|
if p, err = ctx.Plan(); err != nil {
|
|
return state, fmt.Errorf("Error on second follow-up plan: %s", err)
|
|
}
|
|
empty := p.Diff == nil || p.Diff.Empty()
|
|
|
|
// Data resources are tricky because they legitimately get instantiated
|
|
// during refresh so that they will be already populated during the
|
|
// plan walk. Because of this, if we have any data resources in the
|
|
// config we'll end up wanting to destroy them again here. This is
|
|
// acceptable and expected, and we'll treat it as "empty" for the
|
|
// sake of this testing.
|
|
if step.Destroy {
|
|
empty = true
|
|
|
|
for _, moduleDiff := range p.Diff.Modules {
|
|
for k, instanceDiff := range moduleDiff.Resources {
|
|
if !strings.HasPrefix(k, "data.") {
|
|
empty = false
|
|
break
|
|
}
|
|
|
|
if !instanceDiff.Destroy {
|
|
empty = false
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !empty {
|
|
if step.ExpectNonEmptyPlan {
|
|
log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p)
|
|
} else {
|
|
return state, fmt.Errorf(
|
|
"After applying this step and refreshing, "+
|
|
"the plan was not empty:\n\n%s", p)
|
|
}
|
|
}
|
|
|
|
// Made it here, but expected a non-empty plan, fail!
|
|
if step.ExpectNonEmptyPlan && (p.Diff == nil || p.Diff.Empty()) {
|
|
return state, fmt.Errorf("Expected a non-empty plan, but got an empty plan!")
|
|
}
|
|
|
|
// Made it here? Good job test step!
|
|
return state, nil
|
|
}
|
|
|
|
func testStepTaint(state *terraform.State, step TestStep) error {
|
|
for _, p := range step.Taint {
|
|
m := state.RootModule()
|
|
if m == nil {
|
|
return errors.New("no state")
|
|
}
|
|
rs, ok := m.Resources[p]
|
|
if !ok {
|
|
return fmt.Errorf("resource %q not found in state", p)
|
|
}
|
|
log.Printf("[WARN] Test: Explicitly tainting resource %q", p)
|
|
rs.Taint()
|
|
}
|
|
return nil
|
|
}
|