mirror of
https://github.com/opentffoundation/opentf.git
synced 2026-04-05 15:02:02 -04:00
This generalizes the previously-added lint-like check for when an object constructor is used to define an input variable and it contains a definition for an attribute that isn't part of the target type, so that now it also works for various nested structures that commonly arise in real-world configurations. Because this is now considerably more complicated I factored it out into a new package called "lint" which could potentially grow to include other similar "technically valid but probably a mistake" situations in future, but for now it just introduced an opportunity to produce similar warning messages for ignored attribute definitions in the default value for an input variable. It seems to me that there is actually no useful reason to include an unexpected attribute definition in either of these two cases: that attribute will never appear as part of any expression that any other part of the configuration can use. Therefore I considered making these be treated as errors rather than warnings, but turning something that was previously valid into an error is risky so I'm suggesting that we start with these as warnings and then consider upgrading them to errors in a later release if we don't hear of anyone reporting a false-positive that was _somehow_ actually useful. (I find that very unlikely, but still...) Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
300 lines
10 KiB
Go
300 lines
10 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package tofu
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"maps"
|
|
"slices"
|
|
|
|
"github.com/hashicorp/hcl/v2"
|
|
"github.com/zclconf/go-cty/cty"
|
|
|
|
"github.com/opentofu/opentofu/internal/addrs"
|
|
"github.com/opentofu/opentofu/internal/configs"
|
|
"github.com/opentofu/opentofu/internal/dag"
|
|
"github.com/opentofu/opentofu/internal/didyoumean"
|
|
"github.com/opentofu/opentofu/internal/instances"
|
|
"github.com/opentofu/opentofu/internal/lang"
|
|
"github.com/opentofu/opentofu/internal/lang/lint"
|
|
"github.com/opentofu/opentofu/internal/tfdiags"
|
|
)
|
|
|
|
// nodeExpandModuleVariable is the placeholder for an variable that has not yet had
|
|
// its module path expanded.
|
|
type nodeExpandModuleVariable struct {
|
|
Addr addrs.InputVariable
|
|
Module addrs.Module
|
|
Config *configs.Variable
|
|
Expr hcl.Expression
|
|
}
|
|
|
|
var (
|
|
_ GraphNodeDynamicExpandable = (*nodeExpandModuleVariable)(nil)
|
|
_ GraphNodeReferenceOutside = (*nodeExpandModuleVariable)(nil)
|
|
_ GraphNodeReferenceable = (*nodeExpandModuleVariable)(nil)
|
|
_ GraphNodeReferencer = (*nodeExpandModuleVariable)(nil)
|
|
_ graphNodeTemporaryValue = (*nodeExpandModuleVariable)(nil)
|
|
_ graphNodeRetainedByPruneUnusedNodesTransformer = (*nodeExpandModuleVariable)(nil)
|
|
)
|
|
|
|
func (n *nodeExpandModuleVariable) retainDuringUnusedPruning() {}
|
|
|
|
func (n *nodeExpandModuleVariable) temporaryValue(_ walkOperation) bool {
|
|
return true
|
|
}
|
|
|
|
func (n *nodeExpandModuleVariable) DynamicExpand(ctx EvalContext) (*Graph, error) {
|
|
var g Graph
|
|
|
|
expander := ctx.InstanceExpander()
|
|
for _, module := range expander.ExpandModule(n.Module) {
|
|
addr := n.Addr.Absolute(module)
|
|
o := &nodeModuleVariable{
|
|
Addr: addr,
|
|
Config: n.Config,
|
|
Expr: n.Expr,
|
|
ModuleInstance: module,
|
|
}
|
|
g.Add(o)
|
|
}
|
|
addRootNodeToGraph(&g)
|
|
|
|
return &g, nil
|
|
}
|
|
|
|
func (n *nodeExpandModuleVariable) Name() string {
|
|
return fmt.Sprintf("%s.%s (expand, input)", n.Module, n.Addr.String())
|
|
}
|
|
|
|
// GraphNodeModulePath
|
|
func (n *nodeExpandModuleVariable) ModulePath() addrs.Module {
|
|
return n.Module
|
|
}
|
|
|
|
// GraphNodeReferencer
|
|
func (n *nodeExpandModuleVariable) References() []*addrs.Reference {
|
|
// If we have no value expression, we cannot depend on anything.
|
|
if n.Expr == nil {
|
|
return nil
|
|
}
|
|
|
|
// Variables in the root don't depend on anything, because their values
|
|
// are gathered prior to the graph walk and recorded in the context.
|
|
if len(n.Module) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Otherwise, we depend on anything referenced by our value expression.
|
|
// We ignore diagnostics here under the assumption that we'll re-eval
|
|
// all these things later and catch them then; for our purposes here,
|
|
// we only care about valid references.
|
|
//
|
|
// Due to our GraphNodeReferenceOutside implementation, the addresses
|
|
// returned by this function are interpreted in the _parent_ module from
|
|
// where our associated variable was declared, which is correct because
|
|
// our value expression is assigned within a "module" block in the parent
|
|
// module.
|
|
refs, _ := lang.ReferencesInExpr(addrs.ParseRef, n.Expr)
|
|
return refs
|
|
}
|
|
|
|
// GraphNodeReferenceOutside implementation
|
|
func (n *nodeExpandModuleVariable) ReferenceOutside() (selfPath, referencePath addrs.Module) {
|
|
return n.Module, n.Module.Parent()
|
|
}
|
|
|
|
// GraphNodeReferenceable
|
|
func (n *nodeExpandModuleVariable) ReferenceableAddrs() []addrs.Referenceable {
|
|
return []addrs.Referenceable{n.Addr}
|
|
}
|
|
|
|
// nodeModuleVariable represents a module variable input during
|
|
// the apply step.
|
|
type nodeModuleVariable struct {
|
|
Addr addrs.AbsInputVariableInstance
|
|
Config *configs.Variable // Config is the var in the config
|
|
Expr hcl.Expression // Expr is the value expression given in the call
|
|
// ModuleInstance in order to create the appropriate context for evaluating
|
|
// ModuleCallArguments, ex. so count.index and each.key can resolve
|
|
ModuleInstance addrs.ModuleInstance
|
|
}
|
|
|
|
// Ensure that we are implementing all of the interfaces we think we are
|
|
// implementing.
|
|
var (
|
|
_ GraphNodeModuleInstance = (*nodeModuleVariable)(nil)
|
|
_ GraphNodeExecutable = (*nodeModuleVariable)(nil)
|
|
_ graphNodeTemporaryValue = (*nodeModuleVariable)(nil)
|
|
_ dag.GraphNodeDotter = (*nodeModuleVariable)(nil)
|
|
)
|
|
|
|
func (n *nodeModuleVariable) temporaryValue(_ walkOperation) bool {
|
|
return true
|
|
}
|
|
|
|
func (n *nodeModuleVariable) Name() string {
|
|
return n.Addr.String() + "(input)"
|
|
}
|
|
|
|
// GraphNodeModuleInstance
|
|
func (n *nodeModuleVariable) Path() addrs.ModuleInstance {
|
|
// We execute in the parent scope (above our own module) because
|
|
// expressions in our value are resolved in that context.
|
|
return n.Addr.Module.Parent()
|
|
}
|
|
|
|
// GraphNodeModulePath
|
|
func (n *nodeModuleVariable) ModulePath() addrs.Module {
|
|
return n.Addr.Module.Module()
|
|
}
|
|
|
|
// GraphNodeExecutable
|
|
func (n *nodeModuleVariable) Execute(ctx context.Context, evalCtx EvalContext, op walkOperation) (diags tfdiags.Diagnostics) {
|
|
log.Printf("[TRACE] nodeModuleVariable: evaluating %s", n.Addr)
|
|
|
|
val, err := n.evalModuleVariable(ctx, evalCtx, op == walkValidate)
|
|
diags = diags.Append(err)
|
|
if diags.HasErrors() {
|
|
return diags
|
|
}
|
|
|
|
// We might generate some "linter-like" warnings for situations that
|
|
// have a high likelihood of being a mistake even though they are
|
|
// technically valid. We check these only in the validate walk because
|
|
// that always happens before any other walk and so we'd generate
|
|
// duplicate diagnostics if we produced this in later walks too.
|
|
if op == walkValidate {
|
|
diags = diags.Append(n.warningDiags())
|
|
}
|
|
|
|
// Set values for arguments of a child module call, for later retrieval
|
|
// during expression evaluation.
|
|
_, call := n.Addr.Module.CallInstance()
|
|
evalCtx.SetModuleCallArgument(call, n.Addr.Variable, val)
|
|
return diags
|
|
}
|
|
|
|
// dag.GraphNodeDotter impl.
|
|
func (n *nodeModuleVariable) DotNode(name string, opts *dag.DotOpts) *dag.DotNode {
|
|
return &dag.DotNode{
|
|
Name: name,
|
|
Attrs: map[string]string{
|
|
"label": n.Name(),
|
|
"shape": "note",
|
|
},
|
|
}
|
|
}
|
|
|
|
// evalModuleVariable produces the value for a particular variable as will
|
|
// be used by a child module instance.
|
|
//
|
|
// The result is written into a map, with its key set to the local name of the
|
|
// variable, disregarding the module instance address. A map is returned instead
|
|
// of a single value as a result of trying to be convenient for use with
|
|
// EvalContext.SetModuleCallArguments, which expects a map to merge in with any
|
|
// existing arguments.
|
|
//
|
|
// validateOnly indicates that this evaluation is only for config
|
|
// validation, and we will not have any expansion module instance
|
|
// repetition data.
|
|
func (n *nodeModuleVariable) evalModuleVariable(ctx context.Context, evalCtx EvalContext, validateOnly bool) (cty.Value, error) {
|
|
var diags tfdiags.Diagnostics
|
|
var givenVal cty.Value
|
|
var errSourceRange tfdiags.SourceRange
|
|
if expr := n.Expr; expr != nil {
|
|
var moduleInstanceRepetitionData instances.RepetitionData
|
|
|
|
switch {
|
|
case validateOnly:
|
|
// the instance expander does not track unknown expansion values, so we
|
|
// have to assume all RepetitionData is unknown.
|
|
moduleInstanceRepetitionData = instances.RepetitionData{
|
|
CountIndex: cty.UnknownVal(cty.Number),
|
|
EachKey: cty.UnknownVal(cty.String),
|
|
EachValue: cty.DynamicVal,
|
|
}
|
|
|
|
default:
|
|
// Get the repetition data for this module instance,
|
|
// so we can create the appropriate scope for evaluating our expression
|
|
moduleInstanceRepetitionData = evalCtx.InstanceExpander().GetModuleInstanceRepetitionData(n.ModuleInstance)
|
|
}
|
|
|
|
scope := evalCtx.EvaluationScope(nil, nil, moduleInstanceRepetitionData)
|
|
val, moreDiags := scope.EvalExpr(ctx, expr, cty.DynamicPseudoType)
|
|
diags = diags.Append(moreDiags)
|
|
if moreDiags.HasErrors() {
|
|
return cty.DynamicVal, diags.ErrWithWarnings()
|
|
}
|
|
givenVal = val
|
|
errSourceRange = tfdiags.SourceRangeFromHCL(expr.Range())
|
|
} else {
|
|
// We'll use cty.NilVal to represent the variable not being set at all.
|
|
givenVal = cty.NilVal
|
|
errSourceRange = tfdiags.SourceRangeFromHCL(n.Config.DeclRange) // we use the declaration range as a fallback for an undefined variable
|
|
}
|
|
|
|
// We construct a synthetic InputValue here to pretend as if this were
|
|
// a root module variable set from outside, just as a convenience so we
|
|
// can reuse the InputValue type for this.
|
|
rawVal := &InputValue{
|
|
Value: givenVal,
|
|
SourceType: ValueFromConfig,
|
|
SourceRange: errSourceRange,
|
|
}
|
|
|
|
finalVal, moreDiags := prepareFinalInputVariableValue(n.Addr, rawVal, n.Config)
|
|
diags = diags.Append(moreDiags)
|
|
|
|
return finalVal, diags.ErrWithWarnings()
|
|
}
|
|
|
|
// warningDiags detects "lint-like" problems with a variable's definition, where
|
|
// the input is technically valid but nonetheless seems highly likely to be
|
|
// a mistake.
|
|
//
|
|
// This function never returns error diagnostics.
|
|
func (n *nodeModuleVariable) warningDiags() tfdiags.Diagnostics {
|
|
var diags tfdiags.Diagnostics
|
|
|
|
// If the expression used to define the variable includes any object
|
|
// constructor expressions with attribute names that would definitely be
|
|
// discarded during type conversion then we'll warn about that, because
|
|
// there's no useful reason to do that.
|
|
for unused := range lint.DiscardedObjectConstructorAttrs(n.Expr, n.Config.ConstraintType) {
|
|
// The final step of the path is the one representing the problem
|
|
// while any that appear before it are just context.
|
|
prePath, problemStep := unused.Path[:len(unused.Path)-1], unused.Path[len(unused.Path)-1]
|
|
attrName := problemStep.(cty.GetAttrStep).Name
|
|
|
|
attrs := slices.Collect(maps.Keys(unused.TargetType.AttributeTypes()))
|
|
suggestion := ""
|
|
if similarName := didyoumean.NameSuggestion(attrName, attrs); similarName != "" {
|
|
suggestion = fmt.Sprintf(" Did you mean to set attribute %q instead?", similarName)
|
|
}
|
|
|
|
var extraPathClause string
|
|
if len(prePath) != 0 {
|
|
extraPathClause = fmt.Sprintf(" nested value %s", tfdiags.FormatCtyPath(prePath))
|
|
}
|
|
|
|
diags = diags.Append(&hcl.Diagnostic{
|
|
Severity: hcl.DiagWarning,
|
|
Summary: "Object attribute is ignored",
|
|
Detail: fmt.Sprintf(
|
|
"The object type for input variable %q%s does not include an attribute named %q, so this definition is unused.%s",
|
|
n.Addr.Variable.Name, extraPathClause, attrName, suggestion,
|
|
),
|
|
Subject: unused.NameRange.ToHCL().Ptr(),
|
|
})
|
|
}
|
|
|
|
return diags
|
|
}
|