refactor GetResource based on GetModules

Since evaluation is driven only by the configuration (i.e. you can't
interpolate what's not in the config), the resource evaluation should
also follow configuration rather than state. Determining the each mode
solely from the config, and applying that to the state and changes
removes the need for EachMode in the resource state. This bypasses the
awkward dance around getting the correct EachMode set in and retrieved
from state during plan when it changes in the config.
This commit is contained in:
James Bardin 2020-04-22 22:09:47 -04:00
parent 0930f9cd97
commit 5fa90d2032
1 changed files with 102 additions and 174 deletions

View File

@ -470,7 +470,9 @@ func (d *evaluationStateData) GetModule(addr addrs.ModuleCall, rng tfdiags.Sourc
vals[i] = cty.DynamicVal
continue
}
last = i
if i > last {
last = i
}
}
vals = vals[:last+1]
ret = cty.TupleVal(vals)
@ -599,19 +601,7 @@ func (d *evaluationStateData) GetResource(addr addrs.Resource, rng tfdiags.Sourc
return cty.DynamicVal, diags
}
// Break out early during validation, because resource may not be expanded
// yet and indexed references may show up as invalid.
if d.Operation == walkValidate {
return cty.DynamicVal, diags
}
return d.getResourceInstancesAll(addr, rng, config, rs, rs.ProviderConfig)
}
func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng tfdiags.SourceRange, config *configs.Resource, rs *states.Resource, providerAddr addrs.AbsProviderConfig) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
instAddr := addrs.ResourceInstance{Resource: addr, Key: addrs.NoKey}
providerAddr := rs.ProviderConfig
schema := d.getResourceSchema(addr, providerAddr)
if schema == nil {
@ -625,205 +615,143 @@ func (d *evaluationStateData) getResourceInstancesAll(addr addrs.Resource, rng t
})
return cty.DynamicVal, diags
}
ty := schema.ImpliedType()
switch rs.EachMode {
case states.NoEach:
ty := schema.ImpliedType()
is := rs.Instances[addrs.NoKey]
// Decode all instances in the current state
instances := map[addrs.InstanceKey]cty.Value{}
for key, is := range rs.Instances {
if is == nil || is.Current == nil {
// Assume we're dealing with an instance that hasn't been created yet.
return cty.UnknownVal(ty), diags
instances[key] = cty.UnknownVal(ty)
continue
}
instAddr := addr.Instance(key).Absolute(d.ModulePath)
// Planned resources are temporarily stored in state with empty values,
// and need to be replaced bu the planned value here.
if is.Current.Status == states.ObjectPlanned {
// 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(instAddr.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
} else {
// If the object is in planned status then we should not
// get here, since we should've found a pending value
// in the plan above instead.
change := d.Evaluator.Changes.GetResourceInstanceChange(instAddr, states.CurrentGen)
if change == nil {
// If the object is in planned status then we should not get
// here, since we should have found a pending value in the plan
// above instead.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing pending object in plan",
Detail: fmt.Sprintf("Instance %s is marked as having a change pending but that change is not recorded in the plan. This is a bug in Terraform; please report it.", addr),
Detail: fmt.Sprintf("Instance %s is marked as having a change pending but that change is not recorded in the plan. This is a bug in Terraform; please report it.", instAddr),
Subject: &config.DeclRange,
})
return cty.UnknownVal(ty), diags
continue
}
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
}
instances[key] = val
continue
}
ios, err := is.Current.Decode(ty)
if err != nil {
// This shouldn't happen, since by the time we get here
// we should've upgraded the state data already.
// This shouldn't happen, since by the time we get here we
// should have upgraded the state data already.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance data in state",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", addr.Absolute(d.ModulePath), err),
Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", instAddr, err),
Subject: &config.DeclRange,
})
return cty.UnknownVal(ty), diags
continue
}
instances[key] = ios.Value
}
return ios.Value, diags
var ret cty.Value
case states.EachList:
// We need to infer the length of our resulting tuple by searching
// for the max IntKey in our instances map.
length := 0
for k := range rs.Instances {
if ik, ok := k.(addrs.IntKey); ok {
if int(ik) >= length {
length = int(ik) + 1
}
}
}
vals := make([]cty.Value, length)
for i := 0; i < length; i++ {
ty := schema.ImpliedType()
key := addrs.IntKey(i)
is := rs.Instances[key]
if is == nil || is.Current == nil {
// There shouldn't normally be "gaps" in our list but we'll
// allow it under the assumption that we're in a weird situation
// where e.g. someone has run "terraform state mv" to reorder
// a list and left a hole behind.
vals[i] = cty.UnknownVal(schema.ImpliedType())
switch {
case config.Count != nil:
// figure out what the last index we have is
length := -1
for key := range instances {
intKey, ok := key.(addrs.IntKey)
if !ok {
continue
}
if int(intKey) >= length {
length = int(intKey) + 1
}
}
instAddr := addr.Instance(key).Absolute(d.ModulePath)
if is.Current.Status == states.ObjectPlanned {
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
} else {
// If the object is in planned status then we should not
// get here, since we should've found a pending value
// in the plan above instead.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing pending object in plan",
Detail: fmt.Sprintf("Instance %s is marked as having a change pending but that change is not recorded in the plan. This is a bug in Terraform; please report it.", instAddr),
Subject: &config.DeclRange,
})
if length > 0 {
vals := make([]cty.Value, length)
for key, instance := range instances {
intKey, ok := key.(addrs.IntKey)
if !ok {
// old key from state, which isn't valid for evaluation
continue
}
vals[int(intKey)] = instance
}
ios, err := is.Current.Decode(ty)
if err != nil {
// This shouldn't happen, since by the time we get here
// we should've upgraded the state data already.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance data in state",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", instAddr, err),
Subject: &config.DeclRange,
})
// Insert unknown values where there are any missing instances
for i, v := range vals {
if v == cty.NilVal {
vals[i] = cty.UnknownVal(ty)
}
}
ret = cty.TupleVal(vals)
} else {
ret = cty.EmptyTupleVal
}
case config.ForEach != nil:
vals := make(map[string]cty.Value)
for key, instance := range instances {
strKey, ok := key.(addrs.StringKey)
if !ok {
// old key that is being dropped and not used for evaluation
continue
}
vals[i] = ios.Value
vals[string(strKey)] = instance
}
// We use a tuple rather than a list here because resource schemas may
// include dynamically-typed attributes, which will then cause each
// instance to potentially have a different runtime type even though
// they all conform to the static schema.
return cty.TupleVal(vals), diags
case states.EachMap:
ty := schema.ImpliedType()
vals := make(map[string]cty.Value, len(rs.Instances))
for k, is := range rs.Instances {
if sk, ok := k.(addrs.StringKey); ok {
if is == nil || is.Current == nil {
// Assume we're dealing with an instance that hasn't been created yet.
vals[string(sk)] = cty.UnknownVal(schema.ImpliedType())
continue
}
instAddr := addr.Instance(k).Absolute(d.ModulePath)
if is.Current.Status == states.ObjectPlanned {
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
} else {
// If the object is in planned status then we should not
// get here, since we should've found a pending value
// in the plan above instead.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing pending object in plan",
Detail: fmt.Sprintf("Instance %s is marked as having a change pending but that change is not recorded in the plan. This is a bug in Terraform; please report it.", instAddr),
Subject: &config.DeclRange,
})
continue
}
}
ios, err := is.Current.Decode(ty)
if err != nil {
// This shouldn't happen, since by the time we get here
// we should've upgraded the state data already.
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid resource instance data in state",
Detail: fmt.Sprintf("Instance %s data could not be decoded from the state: %s.", instAddr, err),
Subject: &config.DeclRange,
})
continue
}
vals[string(sk)] = ios.Value
}
if len(vals) > 0 {
// We use an object rather than a map here because resource schemas
// may include dynamically-typed attributes, which will then cause
// each instance to potentially have a different runtime type even
// though they all conform to the static schema.
ret = cty.ObjectVal(vals)
} else {
ret = cty.EmptyObjectVal
}
// We use an object rather than a map here because resource schemas may
// include dynamically-typed attributes, which will then cause each
// instance to potentially have a different runtime type even though
// they all conform to the static schema.
return cty.ObjectVal(vals), diags
default:
// Should never happen since caller should deal with other modes
panic(fmt.Sprintf("unsupported EachMode %s", rs.EachMode))
val, ok := instances[addrs.NoKey]
if !ok {
// if the instance is missing, insert an unknown value
val = cty.UnknownVal(ty)
}
ret = val
}
// since the plan was not yet created during validate, the values we
// collected here may not correspond with configuration, so they must be
// unknown.
if d.Operation == walkValidate {
return cty.UnknownVal(ret.Type()), diags
}
return ret, diags
}
func (d *evaluationStateData) getResourceSchema(addr addrs.Resource, providerAddr addrs.AbsProviderConfig) *configschema.Block {