package resource import ( "bufio" "bytes" "errors" "fmt" "log" "sort" "strings" "github.com/hashicorp/terraform/configs/hcl2shim" "github.com/hashicorp/terraform/states" "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/plans" "github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/tfdiags" ) // 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) { if !step.Destroy { if err := testStepTaint(state, step); err != nil { return state, err } } cfg, err := testConfig(opts, step) if err != nil { return state, err } var stepDiags tfdiags.Diagnostics // Build the context opts.Config = cfg opts.State, err = shimLegacyState(state) if err != nil { return nil, err } opts.Destroy = step.Destroy ctx, stepDiags := terraform.NewContext(&opts) if stepDiags.HasErrors() { return state, fmt.Errorf("Error initializing context: %s", stepDiags.Err()) } if stepDiags := ctx.Validate(); len(stepDiags) > 0 { if stepDiags.HasErrors() { return state, errwrap.Wrapf("config is invalid: {{err}}", stepDiags.Err()) } log.Printf("[WARN] Config warnings:\n%s", stepDiags) } // 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! p, stepDiags := ctx.Plan() if stepDiags.HasErrors() { return state, newOperationError("plan", stepDiags) } newState := p.State log.Printf("[WARN] Test: Step plan: %s", legacyPlanComparisonString(newState, p.Changes)) // We need to keep a copy of the state prior to destroying // such that destroy steps can verify their behavior in the check // function stateBeforeApplication := state.DeepCopy() // Apply the diff, creating real resources. newState, stepDiags = ctx.Apply() // shim the state first so the test can check the state on errors state, err = shimNewState(newState, step.providers) if err != nil { return nil, err } if stepDiags.HasErrors() { return state, newOperationError("apply", stepDiags) } // Run any configured checks 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. p, stepDiags := ctx.Plan() if stepDiags.HasErrors() { return state, newOperationError("follow-up plan", stepDiags) } // we don't technically need this any longer with plan handling refreshing, // but run it anyway to ensure the context is working as expected. p, stepDiags = ctx.Plan() if stepDiags.HasErrors() { return state, newOperationError("second follow-up plan", stepDiags) } empty := p.Changes.Empty() newState := p.State if !empty { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) } else { return state, fmt.Errorf( "After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) } } if !empty { if step.ExpectNonEmptyPlan { log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) } else { return state, fmt.Errorf( "After applying this step and refreshing, "+ "the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) } } // Made it here, but expected a non-empty plan, fail! if step.ExpectNonEmptyPlan && 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 } // legacyPlanComparisonString produces a string representation of the changes // from a plan and a given state togther, as was formerly produced by the // String method of terraform.Plan. // // This is here only for compatibility with existing tests that predate our // new plan and state types, and should not be used in new tests. Instead, use // a library like "cmp" to do a deep equality and diff on the two // data structures. func legacyPlanComparisonString(state *states.State, changes *plans.Changes) string { return fmt.Sprintf( "DIFF:\n\n%s\n\nSTATE:\n\n%s", legacyDiffComparisonString(changes), state.String(), ) } // legacyDiffComparisonString produces a string representation of the changes // from a planned changes object, as was formerly produced by the String method // of terraform.Diff. // // This is here only for compatibility with existing tests that predate our // new plan types, and should not be used in new tests. Instead, use a library // like "cmp" to do a deep equality check and diff on the two data structures. func legacyDiffComparisonString(changes *plans.Changes) string { // The old string representation of a plan was grouped by module, but // our new plan structure is not grouped in that way and so we'll need // to preprocess it in order to produce that grouping. type ResourceChanges struct { Current *plans.ResourceInstanceChangeSrc Deposed map[states.DeposedKey]*plans.ResourceInstanceChangeSrc } byModule := map[string]map[string]*ResourceChanges{} resourceKeys := map[string][]string{} requiresReplace := map[string][]string{} var moduleKeys []string for _, rc := range changes.Resources { if rc.Action == plans.NoOp { // We won't mention no-op changes here at all, since the old plan // model we are emulating here didn't have such a concept. continue } moduleKey := rc.Addr.Module.String() if _, exists := byModule[moduleKey]; !exists { moduleKeys = append(moduleKeys, moduleKey) byModule[moduleKey] = make(map[string]*ResourceChanges) } resourceKey := rc.Addr.Resource.String() if _, exists := byModule[moduleKey][resourceKey]; !exists { resourceKeys[moduleKey] = append(resourceKeys[moduleKey], resourceKey) byModule[moduleKey][resourceKey] = &ResourceChanges{ Deposed: make(map[states.DeposedKey]*plans.ResourceInstanceChangeSrc), } } if rc.DeposedKey == states.NotDeposed { byModule[moduleKey][resourceKey].Current = rc } else { byModule[moduleKey][resourceKey].Deposed[rc.DeposedKey] = rc } rr := []string{} for _, p := range rc.RequiredReplace.List() { rr = append(rr, hcl2shim.FlatmapKeyFromPath(p)) } requiresReplace[resourceKey] = rr } sort.Strings(moduleKeys) for _, ks := range resourceKeys { sort.Strings(ks) } var buf bytes.Buffer for _, moduleKey := range moduleKeys { rcs := byModule[moduleKey] var mBuf bytes.Buffer for _, resourceKey := range resourceKeys[moduleKey] { rc := rcs[resourceKey] forceNewAttrs := requiresReplace[resourceKey] crud := "UPDATE" if rc.Current != nil { switch rc.Current.Action { case plans.DeleteThenCreate: crud = "DESTROY/CREATE" case plans.CreateThenDelete: crud = "CREATE/DESTROY" case plans.Delete: crud = "DESTROY" case plans.Create: crud = "CREATE" } } else { // We must be working on a deposed object then, in which // case destroying is the only possible action. crud = "DESTROY" } extra := "" if rc.Current == nil && len(rc.Deposed) > 0 { extra = " (deposed only)" } fmt.Fprintf( &mBuf, "%s: %s%s\n", crud, resourceKey, extra, ) attrNames := map[string]bool{} var oldAttrs map[string]string var newAttrs map[string]string if rc.Current != nil { if before := rc.Current.Before; before != nil { ty, err := before.ImpliedType() if err == nil { val, err := before.Decode(ty) if err == nil { oldAttrs = hcl2shim.FlatmapValueFromHCL2(val) for k := range oldAttrs { attrNames[k] = true } } } } if after := rc.Current.After; after != nil { ty, err := after.ImpliedType() if err == nil { val, err := after.Decode(ty) if err == nil { newAttrs = hcl2shim.FlatmapValueFromHCL2(val) for k := range newAttrs { attrNames[k] = true } } } } } if oldAttrs == nil { oldAttrs = make(map[string]string) } if newAttrs == nil { newAttrs = make(map[string]string) } attrNamesOrder := make([]string, 0, len(attrNames)) keyLen := 0 for n := range attrNames { attrNamesOrder = append(attrNamesOrder, n) if len(n) > keyLen { keyLen = len(n) } } sort.Strings(attrNamesOrder) for _, attrK := range attrNamesOrder { v := newAttrs[attrK] u := oldAttrs[attrK] if v == hcl2shim.UnknownVariableValue { v = "" } // NOTE: we don't support here because we would // need schema to do that. Excluding sensitive values // is now done at the UI layer, and so should not be tested // at the core layer. updateMsg := "" // This may not be as precise as in the old diff, as it matches // everything under the attribute that was originally marked as // ForceNew, but should help make it easier to determine what // caused replacement here. for _, k := range forceNewAttrs { if strings.HasPrefix(attrK, k) { updateMsg = " (forces new resource)" break } } fmt.Fprintf( &mBuf, " %s:%s %#v => %#v%s\n", attrK, strings.Repeat(" ", keyLen-len(attrK)), u, v, updateMsg, ) } } if moduleKey == "" { // root module buf.Write(mBuf.Bytes()) buf.WriteByte('\n') continue } fmt.Fprintf(&buf, "%s:\n", moduleKey) s := bufio.NewScanner(&mBuf) for s.Scan() { buf.WriteString(fmt.Sprintf(" %s\n", s.Text())) } } return buf.String() } 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 }