Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions internal/states/statefile/version4.go
Original file line number Diff line number Diff line change
Expand Up @@ -414,9 +414,7 @@ func writeStateV4(file *File, w io.Writer) tfdiags.Diagnostics {
}
}

if file.State.CheckResults != nil {
sV4.CheckResults = encodeCheckResultsV4(file.State.CheckResults)
}
sV4.CheckResults = encodeCheckResultsV4(file.State.CheckResults)

sV4.normalize()

Expand Down Expand Up @@ -576,6 +574,11 @@ func decodeCheckResultsV4(in []checkResultsV4) (*states.CheckResults, tfdiags.Di
}

func encodeCheckResultsV4(in *states.CheckResults) []checkResultsV4 {
// normalize empty and nil sets in the serialized state
if in == nil || in.ConfigResults.Len() == 0 {
return nil
}

ret := make([]checkResultsV4, 0, in.ConfigResults.Len())

for _, configElem := range in.ConfigResults.Elems {
Expand Down
16 changes: 16 additions & 0 deletions internal/terraform/context_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ func (c *Context) Apply(plan *plans.Plan, config *configs.Config) (*states.State

log.Printf("[DEBUG] Building and walking apply graph for %s plan", plan.UIMode)

// FIXME: refresh plans still store outputs as changes, so we can't use
// Empty()
possibleRefresh := len(plan.Changes.Resources) == 0

graph, operation, diags := c.applyGraph(plan, config, true)
if diags.HasErrors() {
return nil, diags
Expand Down Expand Up @@ -69,6 +73,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 possibleRefresh {
newState.CheckResults = plan.Checks.DeepCopy()
}

return newState, diags
}

Expand Down
143 changes: 143 additions & 0 deletions internal/terraform/context_apply2_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1641,3 +1641,146 @@ 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)
}
}

// NoOp changes may have conditions to evaluate, but should not re-plan and
// apply the entire resource.
func TestContext2Apply_noRePlanNoOp(t *testing.T) {
m := testModuleInline(t, map[string]string{
"main.tf": `
resource "test_object" "x" {
}

resource "test_object" "y" {
# test_object.w is being re-created, so this precondition must be evaluated
# during apply, however this resource should otherwise be a NoOp.
lifecycle {
precondition {
condition = test_object.x.test_string == null
error_message = "test_object.x.test_string should be null"
}
}
}
`})

p := simpleMockProvider()
// make sure we can compute the attr
testString := p.GetProviderSchemaResponse.ResourceTypes["test_object"].Block.Attributes["test_string"]
testString.Computed = true
testString.Optional = false

yAddr := mustResourceInstanceAddr("test_object.y")

state := states.NewState()
mod := state.RootModule()
mod.SetResourceInstanceCurrent(
yAddr.Resource,
&states.ResourceInstanceObjectSrc{
Status: states.ObjectReady,
AttrsJSON: []byte(`{"test_string":"y"}`),
},
mustProviderConfig(`provider["registry.terraform.io/hashicorp/test"]`),
)

ctx := testContext2(t, &ContextOpts{
Providers: map[addrs.Provider]providers.Factory{
addrs.NewDefaultProvider("test"): testProviderFuncFixed(p),
},
})

opts := SimplePlanOpts(plans.NormalMode, nil)
plan, diags := ctx.Plan(m, state, opts)
assertNoErrors(t, diags)

for _, c := range plan.Changes.Resources {
if c.Addr.Equal(yAddr) && c.Action != plans.NoOp {
t.Fatalf("unexpected %s change for test_object.y", c.Action)
}
}

// test_object.y is a NoOp change from the plan, but is included in the
// graph due to the conditions which must be evaluated. This however should
// not cause the resource to be re-planned.
p.PlanResourceChangeFn = func(req providers.PlanResourceChangeRequest) (resp providers.PlanResourceChangeResponse) {
testString := req.ProposedNewState.GetAttr("test_string")
if !testString.IsNull() && testString.AsString() == "y" {
resp.Diagnostics = resp.Diagnostics.Append(errors.New("Unexpected apply-time plan for test_object.y. Original plan was a NoOp"))
}
resp.PlannedState = req.ProposedNewState
return resp
}

_, diags = ctx.Apply(plan, m)
assertNoErrors(t, diags)
}
1 change: 1 addition & 0 deletions internal/terraform/graph_builder_apply.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ func (b *ApplyGraphBuilder) Steps() []GraphTransformer {
Concrete: concreteResourceInstance,
State: b.State,
Changes: b.Changes,
Config: b.Config,
},

// Attach the state
Expand Down
45 changes: 23 additions & 22 deletions internal/terraform/node_resource_abstract_instance.go
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,13 @@ func (n *NodeAbstractResourceInstance) plan(
return plan, state, keyData, diags // failed preconditions prevent further evaluation
}

// If we have a previous plan and the action was a noop, then the only
// reason we're in this method was to evaluate the preconditions. There's
// no need to re-plan this resource.
if plannedChange != nil && plannedChange.Action == plans.NoOp {
return plannedChange, currentState.DeepCopy(), keyData, diags
}

origConfigVal, _, configDiags := ctx.EvaluateBlock(config.Config, schema, nil, keyData)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
Expand Down Expand Up @@ -2059,44 +2066,38 @@ func (n *NodeAbstractResourceInstance) evalDestroyProvisionerConfig(ctx EvalCont
}

// apply accepts an applyConfig, instead of using n.Config, so destroy plans can
// send a nil config. Most of the errors generated in apply are returned as
// diagnostics, but if provider.ApplyResourceChange itself fails, that error is
// returned as an error and nil diags are returned.
// send a nil config. The keyData information can be empty if the config is
// nil, since it is only used to evaluate the configuration.
func (n *NodeAbstractResourceInstance) apply(
ctx EvalContext,
state *states.ResourceInstanceObject,
change *plans.ResourceInstanceChange,
applyConfig *configs.Resource,
createBeforeDestroy bool) (*states.ResourceInstanceObject, instances.RepetitionData, tfdiags.Diagnostics) {
keyData instances.RepetitionData,
createBeforeDestroy bool) (*states.ResourceInstanceObject, tfdiags.Diagnostics) {

var diags tfdiags.Diagnostics
var keyData instances.RepetitionData
if state == nil {
state = &states.ResourceInstanceObject{}
}

if applyConfig != nil {
forEach, _ := evaluateForEachExpression(applyConfig.ForEach, ctx)
keyData = EvalDataForInstanceKey(n.ResourceInstanceAddr().Resource.Key, forEach)
}

if change.Action == plans.NoOp {
// If this is a no-op change then we don't want to actually change
// anything, so we'll just echo back the state we were given and
// let our internal checks and updates proceed.
log.Printf("[TRACE] NodeAbstractResourceInstance.apply: skipping %s because it has no planned action", n.Addr)
return state, keyData, diags
return state, diags
}

provider, providerSchema, err := getProvider(ctx, n.ResolvedProvider)
if err != nil {
return nil, keyData, diags.Append(err)
return nil, diags.Append(err)
}
schema, _ := providerSchema.SchemaForResourceType(n.Addr.Resource.Resource.Mode, n.Addr.Resource.Resource.Type)
if schema == nil {
// Should be caught during validation, so we don't bother with a pretty error here
diags = diags.Append(fmt.Errorf("provider does not support resource type %q", n.Addr.Resource.Resource.Type))
return nil, keyData, diags
return nil, diags
}

log.Printf("[INFO] Starting apply for %s", n.Addr)
Expand All @@ -2107,7 +2108,7 @@ func (n *NodeAbstractResourceInstance) apply(
configVal, _, configDiags = ctx.EvaluateBlock(applyConfig.Config, schema, nil, keyData)
diags = diags.Append(configDiags)
if configDiags.HasErrors() {
return nil, keyData, diags
return nil, diags
}
}

Expand All @@ -2132,13 +2133,13 @@ func (n *NodeAbstractResourceInstance) apply(
strings.Join(unknownPaths, "\n"),
),
))
return nil, keyData, diags
return nil, diags
}

metaConfigVal, metaDiags := n.providerMetas(ctx)
diags = diags.Append(metaDiags)
if diags.HasErrors() {
return nil, keyData, diags
return nil, diags
}

log.Printf("[DEBUG] %s: applying the planned %s change", n.Addr, change.Action)
Expand Down Expand Up @@ -2166,7 +2167,7 @@ func (n *NodeAbstractResourceInstance) apply(
Status: state.Status,
Value: change.After,
}
return newState, keyData, diags
return newState, diags
}

resp := provider.ApplyResourceChange(providers.ApplyResourceChangeRequest{
Expand Down Expand Up @@ -2237,7 +2238,7 @@ func (n *NodeAbstractResourceInstance) apply(
// Bail early in this particular case, because an object that doesn't
// conform to the schema can't be saved in the state anyway -- the
// serializer will reject it.
return nil, keyData, diags
return nil, diags
}

// After this point we have a type-conforming result object and so we
Expand Down Expand Up @@ -2363,7 +2364,7 @@ func (n *NodeAbstractResourceInstance) apply(
// prior state as the new value, making this effectively a no-op. If
// the item really _has_ been deleted then our next refresh will detect
// that and fix it up.
return state.DeepCopy(), keyData, diags
return state.DeepCopy(), diags

case diags.HasErrors() && !newVal.IsNull():
// if we have an error, make sure we restore the object status in the new state
Expand All @@ -2380,7 +2381,7 @@ func (n *NodeAbstractResourceInstance) apply(
newState.Dependencies = state.Dependencies
}

return newState, keyData, diags
return newState, diags

case !newVal.IsNull():
// Non error case with a new state
Expand All @@ -2390,11 +2391,11 @@ func (n *NodeAbstractResourceInstance) apply(
Private: resp.Private,
CreateBeforeDestroy: createBeforeDestroy,
}
return newState, keyData, diags
return newState, diags

default:
// Non error case, were the object was deleted
return nil, keyData, diags
return nil, diags
}
}

Expand Down
Loading