| // 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 |
| } |