X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;ds=sidebyside;f=vendor%2Fgithub.com%2Fhashicorp%2Fterraform%2Fhelper%2Fresource%2Ftesting_config.go;h=311fdb6ef5b64ab06b6e3af47c1a45e9da8b8b28;hb=49c1c7b4dc69ffb9ab52330e6dc52ccdd6351087;hp=537a11c34ae327709f48edc935e11742dfd210c7;hpb=cec3de8a3bcaffd21dedd1bf42da4b490cae7e16;p=github%2Ffretlink%2Fterraform-provider-statuscake.git diff --git a/vendor/github.com/hashicorp/terraform/helper/resource/testing_config.go b/vendor/github.com/hashicorp/terraform/helper/resource/testing_config.go index 537a11c..311fdb6 100644 --- a/vendor/github.com/hashicorp/terraform/helper/resource/testing_config.go +++ b/vendor/github.com/hashicorp/terraform/helper/resource/testing_config.go @@ -1,11 +1,23 @@ package resource import ( + "bufio" + "bytes" + "errors" "fmt" "log" + "sort" "strings" + "github.com/hashicorp/terraform/addrs" + "github.com/hashicorp/terraform/config" + "github.com/hashicorp/terraform/config/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 @@ -16,66 +28,79 @@ func testStepConfig( return testStep(opts, state, step) } -func testStep( - opts terraform.ContextOpts, - state *terraform.State, - step TestStep) (*terraform.State, error) { - mod, err := testModule(opts, 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.Module = mod - opts.State = state - opts.Destroy = step.Destroy - ctx, err := terraform.NewContext(&opts) + opts.Config = cfg + opts.State, err = terraform.ShimLegacyState(state) if err != nil { - return state, fmt.Errorf("Error initializing context: %s", err) + return nil, err } - if ws, es := ctx.Validate(); len(ws) > 0 || len(es) > 0 { - if len(es) > 0 { - estrs := make([]string, len(es)) - for i, e := range es { - estrs[i] = e.Error() - } - return state, fmt.Errorf( - "Configuration is invalid.\n\nWarnings: %#v\n\nErrors: %#v", - ws, estrs) + + 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: %#v", ws) + + log.Printf("[WARN] Config warnings:\n%s", stepDiags) } // Refresh! - state, err = ctx.Refresh() + newState, stepDiags := ctx.Refresh() + // shim the state first so the test can check the state on errors + + state, err = shimNewState(newState, step.providers) if err != nil { - return state, fmt.Errorf( - "Error refreshing: %s", err) + return nil, err + } + if stepDiags.HasErrors() { + return state, newOperationError("refresh", 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! - if p, err := ctx.Plan(); err != nil { - return state, fmt.Errorf( - "Error planning: %s", err) + if p, stepDiags := ctx.Plan(); stepDiags.HasErrors() { + return state, newOperationError("plan", stepDiags) } else { - log.Printf("[WARN] Test: Step plan: %s", p) + 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 behaviour in the check + // such that destroy steps can verify their behavior in the check // function stateBeforeApplication := state.DeepCopy() - // Apply! - state, err = ctx.Apply() + // 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 state, fmt.Errorf("Error applying: %s", err) + return nil, err + } + if stepDiags.HasErrors() { + return state, newOperationError("apply", stepDiags) } - // Check! Excitement! + // Run any configured checks if step.Check != nil { if step.Destroy { if err := step.Check(stateBeforeApplication); err != nil { @@ -91,31 +116,35 @@ func testStep( // 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) + var p *plans.Plan + if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { + return state, newOperationError("follow-up plan", stepDiags) } - if p.Diff != nil && !p.Diff.Empty() { + if !p.Changes.Empty() { if step.ExpectNonEmptyPlan { - log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) + 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", p) + "After applying this step, the plan was not empty:\n\n%s", legacyPlanComparisonString(newState, p.Changes)) } } // And another after a Refresh. if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) { - state, err = ctx.Refresh() + newState, stepDiags = ctx.Refresh() + if stepDiags.HasErrors() { + return state, newOperationError("follow-up refresh", stepDiags) + } + + state, err = shimNewState(newState, step.providers) if err != nil { - return state, fmt.Errorf( - "Error on follow-up refresh: %s", err) + return nil, err } } - if p, err = ctx.Plan(); err != nil { - return state, fmt.Errorf("Error on second follow-up plan: %s", err) + if p, stepDiags = ctx.Plan(); stepDiags.HasErrors() { + return state, newOperationError("second follow-up refresh", stepDiags) } - empty := p.Diff == nil || p.Diff.Empty() + empty := p.Changes.Empty() // Data resources are tricky because they legitimately get instantiated // during refresh so that they will be already populated during the @@ -123,38 +152,254 @@ func testStep( // 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 { + if step.Destroy && !empty { 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 - } + for _, change := range p.Changes.Resources { + if change.Addr.Resource.Resource.Mode != addrs.DataResourceMode { + empty = false + break } } } if !empty { if step.ExpectNonEmptyPlan { - log.Printf("[INFO] Got non-empty plan, as expected:\n\n%s", p) + 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", p) + "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 && (p.Diff == nil || p.Diff.Empty()) { + 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 == config.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 +}