core: Evaluate resource references from plan where possible

Our state representation is not able to preserve unknown values, so it's
not suitable for retaining the transient incomplete values we produce
during planning.

Instead, we'll discard the unknown values when writing to state and have
the expression evaluator prefer an object from the plan where possible.
We still use the shape of the transient state to inform things like the
resource's "each mode", so the plan only masks the object values
themselves.
This commit is contained in:
Martin Atkins 2018-08-28 16:30:52 -07:00
parent 1afdb055ca
commit 20adb9d9b7
3 changed files with 76 additions and 4 deletions

View File

@ -68,7 +68,18 @@ const (
// so the caller must not mutate the receiver any further once once this // so the caller must not mutate the receiver any further once once this
// method is called. // method is called.
func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*ResourceInstanceObjectSrc, error) { func (o *ResourceInstanceObject) Encode(ty cty.Type, schemaVersion uint64) (*ResourceInstanceObjectSrc, error) {
src, err := ctyjson.Marshal(o.Value, ty) // Our state serialization can't represent unknown values, so we convert
// them to nulls here. This is lossy, but nobody should be writing unknown
// values here and expecting to get them out again later.
//
// We get unknown values here while we're building out a "planned state"
// during the plan phase, but the value stored in the plan takes precedence
// for expression evaluation. The apply step should never produce unknown
// values, but if it does it's the responsibility of the caller to detect
// and raise an error about that.
val := cty.UnknownAsNull(o.Value)
src, err := ctyjson.Marshal(val, ty)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View File

@ -16,6 +16,7 @@ import (
"github.com/hashicorp/terraform/configs" "github.com/hashicorp/terraform/configs"
"github.com/hashicorp/terraform/configs/configschema" "github.com/hashicorp/terraform/configs/configschema"
"github.com/hashicorp/terraform/lang" "github.com/hashicorp/terraform/lang"
"github.com/hashicorp/terraform/plans"
"github.com/hashicorp/terraform/states" "github.com/hashicorp/terraform/states"
"github.com/hashicorp/terraform/tfdiags" "github.com/hashicorp/terraform/tfdiags"
) )
@ -54,6 +55,10 @@ type Evaluator struct {
// State is the current state, embedded in a wrapper that ensures that // State is the current state, embedded in a wrapper that ensures that
// it can be safely accessed and modified concurrently. // it can be safely accessed and modified concurrently.
State *states.SyncState State *states.SyncState
// Changes is the set of proposed changes, embedded in a wrapper that
// ensures they can be safely accessed and modified concurrently.
Changes *plans.ChangesSync
} }
// Scope creates an evaluation scope for the given module path and optional // Scope creates an evaluation scope for the given module path and optional
@ -541,6 +546,23 @@ func (d *evaluationStateData) getResourceInstanceSingle(addr addrs.ResourceInsta
return cty.UnknownVal(ty), diags return cty.UnknownVal(ty), diags
} }
// If there's a pending change for this instance in our plan, we'll prefer
// that. This is important because the state can't represent unknown values
// and so its data is inaccurate when changes are pending.
if change := d.Evaluator.Changes.GetResourceInstanceChange(addr.Absolute(d.ModulePath), states.CurrentGen); change != nil {
val, err := change.After.Decode(ty)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance data in plan",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the plan: %s.", addr.Absolute(d.ModulePath), err),
Subject: &config.DeclRange,
})
return cty.UnknownVal(ty), diags
}
return val, diags
}
ios, err := is.Current.Decode(ty) ios, err := is.Current.Decode(ty)
if err != nil { if err != nil {
// This shouldn't happen, since by the time we get here // This shouldn't happen, since by the time we get here
@ -554,7 +576,7 @@ func (d *evaluationStateData) getResourceInstanceSingle(addr addrs.ResourceInsta
return cty.UnknownVal(ty), diags return cty.UnknownVal(ty), diags
} }
return ios.Value, nil return ios.Value, diags
} }
func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng tfdiags.SourceRange, config *configs.Resource, rs *states.Resource, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) { func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng tfdiags.SourceRange, config *configs.Resource, rs *states.Resource, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) {
@ -593,6 +615,25 @@ func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng t
key := addrs.IntKey(i) key := addrs.IntKey(i)
is, exists := rs.Instances[key] is, exists := rs.Instances[key]
if exists { if exists {
instAddr := addr.Instance(key).Absolute(d.ModulePath)
// Prefer pending value in plan if present. See getResourceInstanceSingle
// comment for the rationale.
if change := d.Evaluator.Changes.GetResourceInstanceChange(instAddr, states.CurrentGen); change != nil {
val, err := change.After.Decode(ty)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance data in plan",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the plan: %s.", instAddr, err),
Subject: &config.DeclRange,
})
continue
}
vals[i] = val
continue
}
ios, err := is.Current.Decode(ty) ios, err := is.Current.Decode(ty)
if err != nil { if err != nil {
// This shouldn't happen, since by the time we get here // This shouldn't happen, since by the time we get here
@ -600,7 +641,7 @@ func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng t
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid resource instance data in state", Summary: "Invalid resource instance data in state",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", addr.Instance(key).Absolute(d.ModulePath), err), Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", instAddr, err),
Subject: &config.DeclRange, Subject: &config.DeclRange,
}) })
continue continue
@ -626,6 +667,25 @@ func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng t
vals := make(map[string]cty.Value, len(rs.Instances)) vals := make(map[string]cty.Value, len(rs.Instances))
for k, is := range rs.Instances { for k, is := range rs.Instances {
if sk, ok := k.(addrs.StringKey); ok { if sk, ok := k.(addrs.StringKey); ok {
instAddr := addr.Instance(k).Absolute(d.ModulePath)
// Prefer pending value in plan if present. See getResourceInstanceSingle
// comment for the rationale.
if change := d.Evaluator.Changes.GetResourceInstanceChange(instAddr, states.CurrentGen); change != nil {
val, err := change.After.Decode(ty)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance data in plan",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the plan: %s.", instAddr, err),
Subject: &config.DeclRange,
})
continue
}
vals[string(sk)] = val
continue
}
ios, err := is.Current.Decode(ty) ios, err := is.Current.Decode(ty)
if err != nil { if err != nil {
// This shouldn't happen, since by the time we get here // This shouldn't happen, since by the time we get here
@ -633,7 +693,7 @@ func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng t
diags = diags.Append(&hcl.Diagnostic{ diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError, Severity: hcl.DiagError,
Summary: "Invalid resource instance data in state", Summary: "Invalid resource instance data in state",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", addr.Instance(k).Absolute(d.ModulePath), err), Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", instAddr, err),
Subject: &config.DeclRange, Subject: &config.DeclRange,
}) })
continue continue

View File

@ -68,6 +68,7 @@ func (w *ContextGraphWalker) EnterPath(path addrs.ModuleInstance) EvalContext {
Config: w.Context.config, Config: w.Context.config,
Operation: w.Operation, Operation: w.Operation,
State: w.State, State: w.State,
Changes: w.Changes,
Schemas: w.Context.schemas, Schemas: w.Context.schemas,
VariableValues: w.variableValues, VariableValues: w.variableValues,
VariableValuesLock: &w.variableValuesLock, VariableValuesLock: &w.variableValuesLock,