blob: 270174f8337154e56f440f47a133223ea8bf9c08 [file] [log] [blame] [edit]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package stackeval
import (
"context"
"fmt"
"sync"
"time"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/configs"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/lang/marks"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes"
"github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type EvalPhase rune
//go:generate go tool golang.org/x/tools/cmd/stringer -type EvalPhase
const (
NoPhase EvalPhase = 0
ValidatePhase EvalPhase = 'V'
PlanPhase EvalPhase = 'P'
ApplyPhase EvalPhase = 'A'
// InspectPhase is a special phase that is used only to inspect the
// current dynamic situation, without any intention of changing it.
// This mode allows evaluation against some existing state (possibly
// empty) but cannot plan to make changes nor apply previously-created
// plans.
InspectPhase EvalPhase = 'I'
)
// Referenceable is implemented by types that are identified by the
// implementations of [stackaddrs.Referenceable], returning the value that
// should be used to resolve a reference to that object in an expression
// elsewhere in the configuration.
type Referenceable interface {
// ExprReferenceValue returns the value that a reference to this object
// should resolve to during expression evaluation.
//
// This method cannot fail, because it's not the expression evaluator's
// responsibility to report errors or warnings that might arise while
// processing the target object. Instead, this method will respond to
// internal problems by returning a suitable placeholder value, and
// assume that diagnostics will be returned by another concurrent
// call path.
ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value
}
// ExpressionScope is implemented by types that can have expressions evaluated
// within them, providing the rules for mapping between references in
// expressions to the underlying objects that will provide their values.
type ExpressionScope interface {
// ResolveExpressionReference decides what a particular expression reference
// means in the receiver's evaluation scope and returns the concrete object
// that the address is referring to.
ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics)
// PlanTimestamp returns the timestamp that should be used as part of the
// plantimestamp function in expressions.
PlanTimestamp() time.Time
// ExternalFunctions should return the set of external functions that are
// available to the current scope. The returned function should be called
// when the returned functions are no longer needed.
ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics)
}
// EvalContextForExpr produces an HCL expression evaluation context for the
// given expression in the given evaluation phase within the given expression
// scope.
//
// [EvalExprAndEvalContext] is a convenient wrapper around this which also does
// the final step of evaluating the expression, returning both the value
// and the evaluation context that was used to build it.
func EvalContextForExpr(ctx context.Context, expr hcl.Expression, functions lang.ExternalFuncs, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) {
return evalContextForTraversals(ctx, expr.Variables(), functions, phase, scope)
}
// EvalContextForBody produces an HCL expression context for decoding the
// given [hcl.Body] into a value using the given [hcldec.Spec].
func EvalContextForBody(ctx context.Context, body hcl.Body, spec hcldec.Spec, functions lang.ExternalFuncs, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) {
if body == nil {
panic("EvalContextForBody with nil body")
}
if spec == nil {
panic("EvalContextForBody with nil spec")
}
return evalContextForTraversals(ctx, hcldec.Variables(body, spec), functions, phase, scope)
}
func evalContextForTraversals(ctx context.Context, traversals []hcl.Traversal, functions lang.ExternalFuncs, phase EvalPhase, scope ExpressionScope) (*hcl.EvalContext, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
refs := make(map[stackaddrs.Referenceable]Referenceable)
for _, traversal := range traversals {
ref, _, moreDiags := stackaddrs.ParseReference(traversal)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
continue
}
obj, moreDiags := scope.ResolveExpressionReference(ctx, ref)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
continue
}
refs[ref.Target] = obj
}
if diags.HasErrors() {
return nil, diags
}
varVals := make(map[string]cty.Value)
localVals := make(map[string]cty.Value)
componentVals := make(map[string]cty.Value)
stackVals := make(map[string]cty.Value)
providerVals := make(map[string]map[string]cty.Value)
eachVals := make(map[string]cty.Value)
countVals := make(map[string]cty.Value)
terraformVals := make(map[string]cty.Value)
var selfVal cty.Value
var testOnlyGlobals map[string]cty.Value // allocated only when needed (see below)
for addr, obj := range refs {
val := obj.ExprReferenceValue(ctx, phase)
switch addr := addr.(type) {
case stackaddrs.InputVariable:
varVals[addr.Name] = val
case stackaddrs.LocalValue:
localVals[addr.Name] = val
case stackaddrs.Component:
componentVals[addr.Name] = val
case stackaddrs.StackCall:
stackVals[addr.Name] = val
case stackaddrs.ProviderConfigRef:
if _, exists := providerVals[addr.ProviderLocalName]; !exists {
providerVals[addr.ProviderLocalName] = make(map[string]cty.Value)
}
providerVals[addr.ProviderLocalName][addr.Name] = val
case stackaddrs.ContextualRef:
switch addr {
case stackaddrs.EachKey:
eachVals["key"] = val
case stackaddrs.EachValue:
eachVals["value"] = val
case stackaddrs.CountIndex:
countVals["index"] = val
case stackaddrs.Self:
selfVal = val
case stackaddrs.TerraformApplying:
terraformVals["applying"] = val
default:
// The above should be exhaustive for all values of this enumeration
panic(fmt.Sprintf("unsupported ContextualRef %#v", addr))
}
case stackaddrs.TestOnlyGlobal:
// These are available only to some select unit tests in this
// package, and are not exposed as a real language feature to
// end-users.
if testOnlyGlobals == nil {
testOnlyGlobals = make(map[string]cty.Value)
}
testOnlyGlobals[addr.Name] = val
default:
// The above should cover all possible referenceable address types.
panic(fmt.Sprintf("don't know how to place %T in expression scope", addr))
}
}
providerValVals := make(map[string]cty.Value, len(providerVals))
for k, v := range providerVals {
providerValVals[k] = cty.ObjectVal(v)
}
// HACK: The top-level lang package bundles together the problem
// of resolving variables with the generation of the functions table.
// We only need the functions table here, so we're going to make a
// pseudo-scope just to load the functions from.
// FIXME: Separate these concerns better so that both languages can
// use the same functions but have entirely separate implementations
// of what data is in scope.
fakeScope := &lang.Scope{
Data: nil, // not a real scope; can't actually make an evalcontext
BaseDir: ".",
PureOnly: phase != ApplyPhase,
ConsoleMode: false,
PlanTimestamp: scope.PlanTimestamp(),
ExternalFuncs: functions,
}
hclCtx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(varVals),
"local": cty.ObjectVal(localVals),
"component": cty.ObjectVal(componentVals),
"stack": cty.ObjectVal(stackVals),
"provider": cty.ObjectVal(providerValVals),
},
Functions: fakeScope.Functions(),
}
if len(eachVals) != 0 {
hclCtx.Variables["each"] = cty.ObjectVal(eachVals)
}
if len(countVals) != 0 {
hclCtx.Variables["count"] = cty.ObjectVal(countVals)
}
if len(terraformVals) != 0 {
hclCtx.Variables["terraform"] = cty.ObjectVal(terraformVals)
}
if selfVal != cty.NilVal {
hclCtx.Variables["self"] = selfVal
}
if testOnlyGlobals != nil {
hclCtx.Variables["_test_only_global"] = cty.ObjectVal(testOnlyGlobals)
}
return hclCtx, diags
}
func EvalComponentInputVariables(ctx context.Context, decls map[string]*configs.Variable, wantTy cty.Type, defs *typeexpr.Defaults, decl *stackconfig.Component, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
v := cty.EmptyObjectVal
expr := decl.Inputs
rng := decl.DeclRange
var hclCtx *hcl.EvalContext
if expr != nil {
result, moreDiags := EvalExprAndEvalContext(ctx, expr, phase, scope)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
return cty.DynamicVal, diags
}
expr = result.Expression
hclCtx = result.EvalContext
v = result.Value
rng = tfdiags.SourceRangeFromHCL(result.Expression.Range())
}
if defs != nil {
v = defs.Apply(v)
}
v, err := convert.Convert(v, wantTy)
if err != nil {
// A conversion failure here could either be caused by an author-provided
// expression that's invalid or by the author omitting the argument
// altogether when there's at least one required attribute, so we'll
// return slightly different messages in each case.
if expr != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid inputs for component",
Detail: fmt.Sprintf("Invalid input variable definition object: %s.", tfdiags.FormatError(err)),
Subject: rng.ToHCL().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing required inputs for component",
Detail: fmt.Sprintf("Must provide \"inputs\" argument to define the component's input variables: %s.", tfdiags.FormatError(err)),
Subject: rng.ToHCL().Ptr(),
})
}
return cty.DynamicVal, diags
}
for _, path := range stackconfigtypes.ProviderInstancePathsInValue(v) {
err := path.NewErrorf("cannot send provider configuration reference to Terraform module input variable")
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid inputs for component",
Detail: fmt.Sprintf(
"Invalid input variable definition object: %s.\n\nUse the separate \"providers\" argument to specify the provider configurations to use for this component's root module.",
tfdiags.FormatError(err),
),
Subject: rng.ToHCL().Ptr(),
Expression: expr,
EvalContext: hclCtx,
})
}
if v.IsKnown() && !v.IsNull() {
var markDiags tfdiags.Diagnostics
for varName, varDecl := range decls {
varVal := v.GetAttr(varName)
if !varDecl.Ephemeral {
// If the variable isn't declared as being ephemeral then we
// cannot allow ephemeral values to be assigned to it.
_, markses := varVal.UnmarkDeepWithPaths()
ephemeralPaths, _ := marks.PathsWithMark(markses, marks.Ephemeral)
for _, path := range ephemeralPaths {
if len(path) == 0 {
// The entire value is ephemeral, then.
markDiags = markDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral value not allowed",
Detail: fmt.Sprintf("The input variable %q does not accept ephemeral values.", varName),
Subject: rng.ToHCL().Ptr(),
Expression: expr,
EvalContext: hclCtx,
Extra: diagnosticCausedByEphemeral(true),
})
} else {
// Something nested inside is ephemeral, so we'll be
// more specific.
markDiags = markDiags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Ephemeral value not allowed",
Detail: fmt.Sprintf(
"The input variable %q does not accept ephemeral values, so the value for %s is not compatible.",
varName, tfdiags.FormatCtyPath(path),
),
Subject: rng.ToHCL().Ptr(),
Expression: expr,
EvalContext: hclCtx,
Extra: diagnosticCausedByEphemeral(true),
})
}
}
}
}
diags = diags.Append(markDiags)
if markDiags.HasErrors() {
// If we have an ephemeral value in a place where there shouldn't
// be one then we'll return an entirely-unknown value to make sure
// that downstreams that aren't checking the errors can't leak the
// value into somewhere it ought not to be. We'll still preserve
// the type constraint so that we can do type checking downstream.
return cty.UnknownVal(v.Type()), diags
}
}
return v, diags
}
// EvalExprAndEvalContext evaluates the given HCL expression in the given
// expression scope and returns the resulting value, along with the HCL
// evaluation context that was used to produce it.
//
// This compact helper function is intended for the relatively-common case
// where a caller needs to perform some additional validation on the result
// of the expression which might generate additional diagnostics, and so
// the caller will need the HCL evaluation context in order to construct
// a fully-annotated diagnostic object.
func EvalExprAndEvalContext(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope) (ExprResultValue, tfdiags.Diagnostics) {
functions, diags := scope.ExternalFunctions(ctx)
hclCtx, evalDiags := EvalContextForExpr(ctx, expr, functions, phase, scope)
diags = diags.Append(evalDiags)
if hclCtx == nil {
return ExprResultValue{
Value: cty.NilVal,
Expression: expr,
EvalContext: hclCtx,
}, diags
}
val, hclDiags := expr.Value(hclCtx)
diags = diags.Append(hclDiags)
if val == cty.NilVal {
val = cty.DynamicVal // just so the caller can assume the result is always a value
}
return ExprResultValue{
Value: val,
Expression: expr,
EvalContext: hclCtx,
}, diags
}
// EvalExpr evaluates the given HCL expression in the given expression scope
// and returns the resulting value.
//
// Sometimes callers also need the [hcl.EvalContext] that the expression was
// evaluated with in order to annotate later diagnostics. In that case,
// use [EvalExprAndEvalContext] instead to obtain both the resulting value
// and the evaluation context that was used to produce it.
func EvalExpr(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) {
result, diags := EvalExprAndEvalContext(ctx, expr, phase, scope)
return result.Value, diags
}
// EvalBody evaluates the expressions in the given body using hcldec with
// the given schema, returning the resulting value.
func EvalBody(ctx context.Context, body hcl.Body, spec hcldec.Spec, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) {
functions, diags := scope.ExternalFunctions(ctx)
hclCtx, moreDiags := EvalContextForBody(ctx, body, spec, functions, phase, scope)
diags = diags.Append(moreDiags)
if hclCtx == nil {
return cty.NilVal, diags
}
val, hclDiags := hcldec.Decode(body, spec, hclCtx)
diags = diags.Append(hclDiags)
if val == cty.NilVal {
val = cty.DynamicVal // just so the caller can assume the result is always a value
}
return val, diags
}
// ExprResult bundles an arbitrary result value with the expression and
// evaluation context it was derived from, allowing the recipient to
// potentially emit additional diagnostics if the result is problematic.
//
// (HCL diagnostics related to expressions should typically carry both
// the expression and evaluation context so that we can describe the
// values that were in scope as part of our user-facing diagnostic messages.)
type ExprResult[T any] struct {
Value T
Expression hcl.Expression
EvalContext *hcl.EvalContext
}
// ExprResultValue is an alias for the common case of an expression result
// being a [cty.Value].
type ExprResultValue = ExprResult[cty.Value]
// DerivedExprResult propagates the expression evaluation context through to
// a new result that was presumably derived from the original result but
// still, from a user perspective, associated with the original expression.
func DerivedExprResult[From, To any](from ExprResult[From], newResult To) ExprResult[To] {
return ExprResult[To]{
Value: newResult,
Expression: from.Expression,
EvalContext: from.EvalContext,
}
}
func (r ExprResult[T]) Diagnostic(severity tfdiags.Severity, summary string, detail string) *hcl.Diagnostic {
return &hcl.Diagnostic{
Severity: severity.ToHCL(),
Summary: summary,
Detail: detail,
Subject: r.Expression.Range().Ptr(),
Expression: r.Expression,
EvalContext: r.EvalContext,
}
}
// perEvalPhase is a helper for segregating multiple results for the same
// conceptual operation into a separate result per evaluation phase.
// This is typically needed for any result that's derived from expression
// evaluation, since the values produced for references are constructed
// differently depending on the phase.
//
// This utility works best for types that have a ready-to-use zero value.
type perEvalPhase[T any] struct {
mu sync.Mutex
vals map[EvalPhase]*T
}
// For returns a pointer to the value belonging to the given evaluation phase,
// automatically allocating a new zero-value T if this is the first call for
// the given phase.
//
// This method is itself safe to call concurrently, but it does not constrain
// access to the returned value, and so interaction with that object may
// require additional care depending on the definition of T.
func (pep *perEvalPhase[T]) For(phase EvalPhase) *T {
if phase == NoPhase {
// Asking for the value for no phase at all is a nonsense.
panic("perEvalPhase.For(NoPhase)")
}
pep.mu.Lock()
if pep.vals == nil {
pep.vals = make(map[EvalPhase]*T)
}
if _, exists := pep.vals[phase]; !exists {
pep.vals[phase] = new(T)
}
ret := pep.vals[phase]
pep.mu.Unlock()
return ret
}
// Each calls the given reporting callback for all of the values the
// receiver is currently tracking.
//
// Each blocks calls to the For method throughout its execution, so callback
// functions must not interact with the receiver to avoid a deadlock.
func (pep *perEvalPhase[T]) Each(report func(EvalPhase, *T)) {
pep.mu.Lock()
for phase, val := range pep.vals {
report(phase, val)
}
pep.mu.Unlock()
}
// JustValue is a special implementation of [Referenceable] used in special
// situations where an [ExpressionScope] needs to just return a specific
// value directly, rather athn indirect through some other referencable object
// for dynamic value resolution.
type JustValue struct {
v cty.Value
}
var _ Referenceable = JustValue{}
// ExprReferenceValue implements Referenceable.
func (jv JustValue) ExprReferenceValue(context.Context, EvalPhase) cty.Value {
return jv.v
}