terraform: a lang.Data impl for unexpanded modules

When we're evaluating expressions inside a module that's beneath a
partially-expanded prefix, we need to use placeholder values that describe
only what we know to be true for all possible instances within that prefix,
which requires quite a different reference evaluation strategy than we'd
use in a fully-expanded module instance.

evaluationPlaceholderData will therefore substitute for evaluationStateData
in such cases. It is associated with an unbounded set of possible module
instances sharing a known module instance prefix, and uses placeholder
data generated by special graph nodes representing unexpanded objects,
instead of finalized data coming from the main state or plan.

As of this commit, the new type is not used at all. In subsequent commits
we'll teach terraform.EvalContextBuiltin to support working under
unexpanded module prefixes, and then this new lang.Data type will be part
of the implementation of its expression-evaluation methods in that case.
pull/34578/head
Martin Atkins 2 years ago
parent 3040deac66
commit 39a6f3a6db

@ -0,0 +1,275 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package terraform
import (
"fmt"
"github.com/zclconf/go-cty/cty"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/addrs"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// evaluationPlaceholderData is an implementation of lang.Data that deals
// with resolving references inside module prefixes whose full expansion
// isn't known yet, and thus returns placeholder values that represent
// only what we know to be true for all possible final module instances
// that could exist for the prefix.
type evaluationPlaceholderData struct {
Evaluator *Evaluator
// ModulePath is the partially-expanded path through the dynamic module
// tree to a set of possible module instances that share a common known
// prefix.
ModulePath addrs.PartialExpandedModule
// CountAvailable is true if this data object is representing an evaluation
// scope where the "count" symbol would be available.
CountAvailable bool
// EachAvailable is true if this data object is representing an evaluation
// scope where the "each" symbol would be available.
EachAvailable bool
// Operation records the type of walk the evaluationStateData is being used
// for.
Operation walkOperation
}
// TODO: Historically we were inconsistent about whether static validation
// logic is implemented in Evaluator.StaticValidateReference or inline in
// methods of evaluationStateData, because the dedicated static validator
// came later.
//
// Some validation rules (and their associated error messages) have therefore
// ended up being duplicated between evaluationPlaceholderData and
// evaluationStateData. We've accepted that for now to avoid creating a bunch
// of churn in pre-existing code while adding support for partial expansion
// placeholders, but one day it would be nice to refactor this a little so
// that the division between these three units is a little clearer and so
// that all of the error checks are implemented in only one place each.
var _ lang.Data = (*evaluationPlaceholderData)(nil)
// GetCheckBlock implements lang.Data.
func (d *evaluationPlaceholderData) GetCheckBlock(addr addrs.Check, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// check blocks don't produce any useful data and can only be referred
// to within an expect_failures attribute in the test language.
var diags tfdiags.Diagnostics
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to \"check\" in invalid context",
Detail: "The \"check\" object can only be used from an \"expect_failures\" attribute within a Terraform testing \"run\" block.",
Subject: rng.ToHCL().Ptr(),
})
return cty.NilVal, diags
}
// GetCountAttr implements lang.Data.
func (d *evaluationPlaceholderData) GetCountAttr(addr addrs.CountAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
switch addr.Name {
case "index":
if !d.CountAvailable {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to "count" in non-counted context`,
Detail: `The "count" object can only be used in "module", "resource", and "data" blocks, and only when the "count" argument is set.`,
Subject: rng.ToHCL().Ptr(),
})
}
// When we're under a partially-expanded prefix, the leaf instance
// keys are never known because otherwise we'd be under a fully-known
// prefix by definition. We do know it's always >= 0 and not null,
// though.
return cty.UnknownVal(cty.Number).Refine().
NumberRangeLowerBound(cty.Zero, true).
NotNull().
NewValue(), diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "count" attribute`,
Detail: fmt.Sprintf(`The "count" object does not have an attribute named %q. The only supported attribute is count.index, which is the index of each instance of a resource block that has the "count" argument set.`, addr.Name),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
}
// GetForEachAttr implements lang.Data.
func (d *evaluationPlaceholderData) GetForEachAttr(addr addrs.ForEachAttr, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// When we're under a partially-expanded prefix, the leaf instance
// keys are never known because otherwise we'd be under a fully-known
// prefix by definition. Therefore all return paths here produce unknown
// values.
if !d.EachAvailable {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Reference to "each" in context without for_each`,
Detail: `The "each" object can be used only in "module" or "resource" blocks, and only when the "for_each" argument is set.`,
Subject: rng.ToHCL().Ptr(),
})
return cty.UnknownVal(cty.DynamicPseudoType), diags
}
switch addr.Name {
case "key":
// each.key is always a string and is never null
return cty.UnknownVal(cty.String).RefineNotNull(), diags
case "value":
return cty.DynamicVal, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: `Invalid "each" attribute`,
Detail: fmt.Sprintf(`The "each" object does not have an attribute named %q. The supported attributes are each.key and each.value, the current key and value pair of the "for_each" attribute set.`, addr.Name),
Subject: rng.ToHCL().Ptr(),
})
return cty.DynamicVal, diags
}
}
// GetInputVariable implements lang.Data.
func (d *evaluationPlaceholderData) GetInputVariable(addr addrs.InputVariable, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
namedVals := d.Evaluator.NamedValues
absAddr := addrs.ObjectInPartialExpandedModule(d.ModulePath, addr)
return namedVals.GetInputVariablePlaceholder(absAddr), nil
}
// GetLocalValue implements lang.Data.
func (d *evaluationPlaceholderData) GetLocalValue(addr addrs.LocalValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
namedVals := d.Evaluator.NamedValues
absAddr := addrs.ObjectInPartialExpandedModule(d.ModulePath, addr)
return namedVals.GetLocalValuePlaceholder(absAddr), nil
}
// GetModule implements lang.Data.
func (d *evaluationPlaceholderData) GetModule(addr addrs.ModuleCall, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// We'll reuse the evaluator's "static evaluate" logic to check that the
// module call being referred to is even declared in the configuration,
// since it returns a good-quality error message for that case that
// we don't want to have to duplicate here.
diags := d.Evaluator.StaticValidateReference(&addrs.Reference{
Subject: addr,
SourceRange: rng,
}, d.ModulePath.Module(), nil, nil)
if diags.HasErrors() {
return cty.DynamicVal, diags
}
callerCfg := d.Evaluator.Config.Descendent(d.ModulePath.Module())
if callerCfg == nil {
// Strange! The above StaticValidateReference should've failed if
// the module we're in isn't even declared. But we'll just tolerate
// it and return a very general placeholder.
return cty.DynamicVal, diags
}
callCfg := callerCfg.Module.ModuleCalls[addr.Name]
if callCfg == nil {
// Again strange, for the same reason as just above.
return cty.DynamicVal, diags
}
// Any module call under an unexpanded prefix has an unknown set of instance
// keys itself by definition, unless that call isn't using count or for_each
// at all and thus we know it has exactly one "no-key" instance.
//
// If we don't know the instance keys then we cannot predict anything about
// the result, because module calls with repetition appear as either
// object or tuple types and we cannot predict those types here.
if callCfg.Count != nil || callCfg.ForEach != nil {
return cty.DynamicVal, diags
}
// If we get down here then we know we have a single-instance module, and
// so we can return a more specific placeholder object that has all of
// the child module's declared output values represented, which could
// then potentially allow detecting a downstream error referring to
// an output value that doesn't actually exist.
calledCfg := d.Evaluator.Config.Descendent(d.ModulePath.Module().Child(addr.Name))
if calledCfg == nil {
// This suggests that the config wasn't constructed correctly, since
// there should always be a child config node for any module call,
// but that's a "package configs" problem and so we'll just tolerate
// it here for robustness.
return cty.DynamicVal, diags
}
attrs := make(map[string]cty.Value, len(calledCfg.Module.Outputs))
for name := range calledCfg.Module.Outputs {
// Module output values are dynamically-typed, so we cannot
// predict anything about their results until finalized.
attrs[name] = cty.DynamicVal
}
return cty.ObjectVal(attrs), diags
}
// GetOutput implements lang.Data.
func (d *evaluationPlaceholderData) GetOutput(addr addrs.OutputValue, rng tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
namedVals := d.Evaluator.NamedValues
absAddr := addrs.ObjectInPartialExpandedModule(d.ModulePath, addr)
return namedVals.GetOutputValuePlaceholder(absAddr), nil
}
// GetPathAttr implements lang.Data.
func (d *evaluationPlaceholderData) GetPathAttr(addrs.PathAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO: It would be helpful to perform the same logic here as we do
// in the full-evaluation case, since the paths we'd return here cannot
// vary based on dynamic data, but we'll need to factor out the logic
// into a common location we can call from both places first. For now,
// we'll just leave these all as unknown value placeholders.
//
// What we _do_ know is that all valid attributes of "path" are strings
// that are definitely not null, so we can at least catch situations
// where someone tries to use them in a place where a string is
// unacceptable.
return cty.UnknownVal(cty.String).RefineNotNull(), nil
}
// GetResource implements lang.Data.
func (d *evaluationPlaceholderData) GetResource(addrs.Resource, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO: Once we've implemented the evaluation of placeholders for
// deferred resources during the graph walk, we should return such
// placeholders here where possible.
//
// However, for resources that use count or for_each we'd not be able
// to predict anything more than cty.DynamicVal here anyway, since
// we don't know the instance keys, and so that improvement would only
// really help references to single-instance resources.
return cty.DynamicVal, nil
}
// GetRunBlock implements lang.Data.
func (d *evaluationPlaceholderData) GetRunBlock(addrs.Run, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// We should not get here because any scope that has an [evaluationPlaceholderData]
// as its Data should have a reference parser that doesn't accept addrs.Run
// addresses.
panic("GetRunBlock called on non-test evaluation dataset")
}
// GetTerraformAttr implements lang.Data.
func (d *evaluationPlaceholderData) GetTerraformAttr(addrs.TerraformAttr, tfdiags.SourceRange) (cty.Value, tfdiags.Diagnostics) {
// TODO: It would be helpful to perform the same validation checks that
// occur in evaluationStateData.GetTerraformAttr, so authors can catch
// invalid usage of the "terraform" object even when under an unexpanded
// module prefix.
return cty.DynamicVal, nil
}
// StaticValidateReferences implements lang.Data.
func (d *evaluationPlaceholderData) StaticValidateReferences(refs []*addrs.Reference, self addrs.Referenceable, source addrs.Referenceable) tfdiags.Diagnostics {
return d.Evaluator.StaticValidateReferences(refs, d.ModulePath.Module(), self, source)
}
Loading…
Cancel
Save