Merge pull request #30916 from hashicorp/jbardin/speculative-apply-graph

core: build a test apply graph during plan
This commit is contained in:
James Bardin 2022-04-22 16:08:44 -04:00 committed by GitHub
commit 1252f8c9c5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 73 additions and 0 deletions

View File

@ -204,9 +204,25 @@ The -target option is not for routine use, and is provided only for exceptional
diags = diags.Append(rDiags)
plan.RelevantAttributes = relevantAttrs
diags = diags.Append(c.checkApplyGraph(plan, config))
return plan, diags
}
// checkApplyGraph builds the apply graph out of the current plan to
// check for any errors that may arise once the planned changes are added to
// the graph. This allows terraform to report errors (mostly cycles) during
// plan that would otherwise only crop up during apply
func (c *Context) checkApplyGraph(plan *plans.Plan, config *configs.Config) tfdiags.Diagnostics {
if plan.Changes.Empty() {
log.Println("[DEBUG] no planned changes, skipping apply graph check")
return nil
}
log.Println("[DEBUG] building apply graph to check for errors")
_, _, diags := c.applyGraph(plan, config, true)
return diags
}
var DefaultPlanOpts = &PlanOpts{
Mode: plans.NormalMode,
}

View File

@ -3096,3 +3096,60 @@ data "test_object" "a" {
_, diags := ctx.Plan(m, state, DefaultPlanOpts)
assertNoErrors(t, diags)
}
func TestContext2Plan_applyGraphError(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "a" {
}
resource "test_object" "b" {
depends_on = [test_object.a]
}
`,
})
p := simpleMockProvider()
// Here we introduce a cycle via state which only shows up in the apply
// graph where the actual destroy instances are connected in the graph.
// This could happen for example when a user has an existing state with
// stored dependencies, and changes the config in such a way that
// contradicts the stored dependencies.
state := states.NewState()
root := state.EnsureModule(addrs.RootModuleInstance)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.a").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"test_string":"a"}`),
Dependencies: []addrs.ConfigResource{mustResourceInstanceAddr("test_object.b").ContainingResource().Config()},
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
root.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.b").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectTainted,
AttrsJSON: []byte(`{"test_string":"b"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
_, diags := ctx.Plan(m, state, &PlanOpts{
Mode: plans.NormalMode,
})
if !diags.HasErrors() {
t.Fatal("cycle error not detected")
}
msg := diags.ErrWithWarnings().Error()
if !strings.Contains(msg, "Cycle") {
t.Fatalf("no cycle error found:\n got: %s\n", msg)
}
}