| // 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 ( | |
| _ graphNodeVariableConfig = (*nodeModuleVariable)(nil) | |
| _ 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(), | |
| Context: unused.ContextRange.ToHCL().Ptr(), | |
| }) | |
| } | |
| return diags | |
| } | |
| // graphNodeVariableConfig | |
| func (n *nodeModuleVariable) VariableConfig() *configs.Variable { | |
| return n.Config | |
| } |