don't lose checks from refresh-only plan

If there are no changes, then there is no reason to create an apply
graph since all objects are known. We however do need the walk to match
the expected state structure. This is probably only cleanup of empty
nested modules and outputs, but some investigation is needed before
making the full change.

For now we can store the checks from the plan directly into the new
state, since the apply walk overwrote the results we had already.
This commit is contained in:
James Bardin 2022-10-28 13:57:51 -04:00
parent ff68c8d129
commit b61c02da05
2 changed files with 84 additions and 0 deletions

View File

@ -69,6 +69,18 @@ Note that the -target option is not suitable for routine use, and is provided on
))
}
// FIXME: we cannot check for an empty plan for refresh-only, because root
// outputs are always stored as changes. The final condition of the state
// also depends on some cleanup which happens during the apply walk. It
// would probably make more sense if applying a refresh-only plan were
// simply just returning the planned state and checks, but some extra
// cleanup is going to be needed to make the plan state match what apply
// would do. For now we can copy the checks over which were overwritten
// during the apply walk.
if len(plan.Changes.Resources) == 0 {
newState.CheckResults = plan.Checks.DeepCopy()
}
return newState, diags
}

View File

@ -1641,3 +1641,75 @@ output "from_resource" {
_, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
}
// -refresh-only should update checks
func TestContext2Apply_refreshApplyUpdatesChecks(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "x" {
test_string = "ok"
lifecycle {
postcondition {
condition = self.test_string == "ok"
error_message = "wrong val"
}
}
}
output "from_resource" {
value = test_object.x.test_string
precondition {
condition = test_object.x.test_string == "ok"
error_message = "wrong val"
}
}
`})
p := simpleMockProvider()
p.ReadResourceResponse = &providers.ReadResourceResponse{
NewState: cty.ObjectVal(map[string]cty.Value{
"test_string": cty.StringVal("ok"),
}),
}
state := states.NewState()
mod := state.EnsureModule(addrs.RootModuleInstance)
mod.SetResourceInstanceCurrent(
mustResourceInstanceAddr("test_object.x").Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"test_string":"wrong val"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)
mod.SetOutputValue("from_resource", cty.StringVal("wrong val"), false)
ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})
opts := SimplePlanOpts(plans.RefreshOnlyMode, nil)
plan, diags := ctx.Plan(m, state, opts)
assertNoErrors(t, diags)
state, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
resCheck := state.CheckResults.GetObjectResult(mustResourceInstanceAddr("test_object.x"))
if resCheck.Status != checks.StatusPass {
t.Fatalf("unexpected check %s: %s\n", resCheck.Status, resCheck.FailureMessages)
}
outAddr := addrs.AbsOutputValue{
Module: addrs.RootModuleInstance,
OutputValue: addrs.OutputValue{
Name: "from_resource",
},
}
outCheck := state.CheckResults.GetObjectResult(outAddr)
if outCheck.Status != checks.StatusPass {
t.Fatalf("unexpected check %s: %s\n", outCheck.Status, outCheck.FailureMessages)
}
}