blob: 620b64cece032ef9cc00d48427d38f764a9829a8 [file] [log] [blame]
// 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 {
*evaluationData
// 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)
// 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.Descendant(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.Descendant(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
}
// 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
}