stackruntime: Stubbing the "interpreter" for stack configurations

pull/34738/head
Martin Atkins 3 years ago
parent 41d931d21a
commit 3d503f8e71

@ -11,6 +11,10 @@ func (Component) referenceableSigil() {}
func (Component) inStackConfigSigil() {}
func (Component) inStackInstanceSigil() {}
func (c Component) String() string {
return "component." + c.Name
}
// ConfigComponent places a [Component] in the context of a particular [Stack].
type ConfigComponent = InStackConfig[Component]

@ -40,3 +40,13 @@ func Absolute[T StackItemDynamic](stackAddr StackInstance, relAddr T) InStackIns
Item: relAddr,
}
}
// ConfigForAbs returns the "in stack config" equivalent of the given
// "in stack instance" (absolute) address by just discarding any
// instance keys from the stack instance steps.
func ConfigForAbs[T interface {
StackItemDynamic
StackItemConfig
}](absAddr InStackInstance[T]) InStackConfig[T] {
return Config(absAddr.Stack.ConfigAddr(), absAddr.Item)
}

@ -8,6 +8,10 @@ func (InputVariable) referenceableSigil() {}
func (InputVariable) inStackConfigSigil() {}
func (InputVariable) inStackInstanceSigil() {}
func (v InputVariable) String() string {
return "var." + v.Name
}
// ConfigInputVariable places an [InputVariable] in the context of a particular [Stack].
type ConfigInputVariable = InStackConfig[InputVariable]

@ -8,6 +8,10 @@ func (LocalValue) referenceableSigil() {}
func (LocalValue) inStackConfigSigil() {}
func (LocalValue) inStackInstanceSigil() {}
func (v LocalValue) String() string {
return "local." + v.Name
}
// ConfigLocalValue places a [LocalValue] in the context of a particular [Stack].
type ConfigLocalValue = InStackConfig[LocalValue]

@ -7,6 +7,10 @@ type OutputValue struct {
func (OutputValue) inStackConfigSigil() {}
func (OutputValue) inStackInstanceSigil() {}
func (v OutputValue) String() string {
return "output." + v.Name
}
// ConfigOutputValue places an [OutputValue] in the context of a particular [Stack].
type ConfigOutputValue = InStackConfig[OutputValue]

@ -18,6 +18,10 @@ type ProviderConfigRef struct {
func (ProviderConfigRef) referenceableSigil() {}
func (r ProviderConfigRef) String() string {
return "provider." + r.ProviderLocalName + "." + r.Name
}
// ProviderConfig is the address of a "provider" block in a stack configuration.
type ProviderConfig struct {
Provider addrs.Provider

@ -0,0 +1,89 @@
package stackaddrs
import (
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Reference describes a reference expression found in the configuration,
// capturing what it referred to and where it was found in source code.
type Reference struct {
Target Referenceable
SourceRange tfdiags.SourceRange
}
// ParseReference raises a raw absolute traversal into a higher-level reference,
// or returns error diagnostics explaining why it cannot.
//
// The returned traversal is a relative traversal covering the remainder of
// the given traversal after the part captured into the returned reference,
// in case the caller wants to do further validation or analysis of the
// subsequent steps.
func ParseReference(traversal hcl.Traversal) (Reference, hcl.Traversal, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
var ret Reference
switch rootName := traversal.RootName(); rootName {
case "var":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = InputVariable{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "local":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = LocalValue{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "component":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = Component{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
case "stack":
name, rng, remain, diags := parseSingleAttrRef(traversal)
ret.Target = StackCall{Name: name}
ret.SourceRange = tfdiags.SourceRangeFromHCL(rng)
return ret, remain, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to unknown symbol",
Detail: fmt.Sprintf("There is no symbol %q defined in the current scope.", rootName),
Subject: traversal[0].SourceRange().Ptr(),
})
return ret, nil, diags
}
}
func parseSingleAttrRef(traversal hcl.Traversal) (string, hcl.Range, hcl.Traversal, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
root := traversal.RootName()
rootRange := traversal[0].SourceRange()
if len(traversal) < 2 {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: fmt.Sprintf("The %q object cannot be accessed directly. Instead, access one of its attributes.", root),
Subject: &rootRange,
})
return "", hcl.Range{}, nil, diags
}
if attrTrav, ok := traversal[1].(hcl.TraverseAttr); ok {
return attrTrav.Name, hcl.RangeBetween(rootRange, attrTrav.SrcRange), traversal[2:], diags
}
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: fmt.Sprintf("The %q object does not support this operation.", root),
Subject: traversal[1].SourceRange().Ptr(),
})
return "", hcl.Range{}, nil, diags
}

@ -4,6 +4,7 @@ package stackaddrs
// the target of an expression-based reference within a particular stack.
type Referenceable interface {
referenceableSigil()
String() string
}
var _ Referenceable = Component{}

@ -95,3 +95,15 @@ func (s StackInstance) Call() AbsStackCall {
},
}
}
// ConfigAddr returns the [Stack] corresponding to the receiving [StackInstance].
func (s StackInstance) ConfigAddr() Stack {
if s.IsRoot() {
return RootStack
}
ret := make(Stack, len(s))
for i, step := range s {
ret[i] = StackStep{Name: step.Name}
}
return ret
}

@ -13,6 +13,10 @@ func (StackCall) referenceableSigil() {}
func (StackCall) inStackConfigSigil() {}
func (StackCall) inStackInstanceSigil() {}
func (c StackCall) String() string {
return "stack." + c.Name
}
// ConfigStackCall represents a static stack call inside a particular [Stack].
type ConfigStackCall = InStackConfig[StackCall]

@ -0,0 +1,4 @@
// Package stackruntime contains the runtime implementation of the Stacks
// language, allowing the various operations over a stack configuration and
// its associated state and plans.
package stackruntime

@ -0,0 +1,3 @@
package stackeval
type ApplyOpts struct{}

@ -0,0 +1,8 @@
package stackeval
import "github.com/hashicorp/terraform/internal/tfdiags"
type withDiagnostics[T any] struct {
Result T
Diagnostics tfdiags.Diagnostics
}

@ -0,0 +1,13 @@
// Package stackeval contains all of the internal logic of the stacks language
// runtime.
//
// This package may be imported only by the "stackruntime" package. It's a
// separate package only so we can draw a distinction between symbols that
// are exported to stackruntime vs. symbols that are private to this package,
// since there are lots of symbols in here.
//
// All functions in this package which take a [context.Context] value require
// that context to represent a task started by the package "promising", and
// may use the given task context to create promises and then wait for and/or
// resolve them. Calling with a non-task context will typically panic.
package stackeval

@ -0,0 +1,37 @@
// Code generated by "stringer -type EvalPhase"; DO NOT EDIT.
package stackeval
import "strconv"
func _() {
// An "invalid array index" compiler error signifies that the constant values have changed.
// Re-run the stringer command to generate them again.
var x [1]struct{}
_ = x[NoPhase-0]
_ = x[ValidatePhase-86]
_ = x[PlanPhase-80]
_ = x[ApplyPhase-65]
}
const (
_EvalPhase_name_0 = "NoPhase"
_EvalPhase_name_1 = "ApplyPhase"
_EvalPhase_name_2 = "PlanPhase"
_EvalPhase_name_3 = "ValidatePhase"
)
func (i EvalPhase) String() string {
switch {
case i == 0:
return _EvalPhase_name_0
case i == 65:
return _EvalPhase_name_1
case i == 80:
return _EvalPhase_name_2
case i == 86:
return _EvalPhase_name_3
default:
return "EvalPhase(" + strconv.FormatInt(int64(i), 10) + ")"
}
}

@ -0,0 +1,132 @@
package stackeval
import (
"context"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/lang"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
)
type EvalPhase rune
//go:generate go run golang.org/x/tools/cmd/stringer -type EvalPhase
const (
NoPhase EvalPhase = 0
ValidatePhase EvalPhase = 'V'
PlanPhase EvalPhase = 'P'
ApplyPhase EvalPhase = 'A'
)
// 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)
}
// EvalExpr evaluates the given HCL expression in the given expression scope
// and returns the resulting value.
func EvalExpr(ctx context.Context, expr hcl.Expression, phase EvalPhase, scope ExpressionScope) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
traversals := expr.Variables()
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 cty.DynamicVal, 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)
// TODO: Also providerVals
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:
// TODO: Implement
panic(fmt.Sprintf("don't know how to place %T in expression scope", addr))
default:
// The above should cover all possible referenceable address types.
panic(fmt.Sprintf("don't know how to place %T in expression scope", addr))
}
}
// 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,
// TODO: PlanTimestamp
}
hclCtx := &hcl.EvalContext{
Variables: map[string]cty.Value{
"var": cty.ObjectVal(varVals),
"local": cty.ObjectVal(localVals),
"component": cty.ObjectVal(componentVals),
"stack": cty.ObjectVal(stackVals),
// TODO: "provider": cty.ObjectVal(providerVals),
},
Functions: fakeScope.Functions(),
}
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 val, diags
}

@ -0,0 +1,16 @@
package stackeval
import (
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
)
// InputVariable represents an input variable belonging to a [Stack].
type InputVariable struct {
addr stackaddrs.AbsInputVariable
main *Main
}
func (v *InputVariable) Addr() stackaddrs.AbsInputVariable {
return v.addr
}

@ -0,0 +1,135 @@
package stackeval
import (
"context"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/typeexpr"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// InputVariableConfig represents a "variable" block in a stack configuration.
type InputVariableConfig struct {
addr stackaddrs.ConfigInputVariable
config *stackconfig.InputVariable
main *Main
}
var _ Validatable = (*InputVariableConfig)(nil)
var _ Referenceable = (*InputVariableConfig)(nil)
func newInputVariableConfig(main *Main, addr stackaddrs.ConfigInputVariable, config *stackconfig.InputVariable) *InputVariableConfig {
return &InputVariableConfig{
addr: addr,
config: config,
main: main,
}
}
func (v *InputVariableConfig) Addr() stackaddrs.ConfigInputVariable {
return v.addr
}
func (v *InputVariableConfig) TypeConstraint() cty.Type {
return v.config.Type.Constraint
}
func (v *InputVariableConfig) NestedDefaults() *typeexpr.Defaults {
return v.config.Type.Defaults
}
// DefaultValue returns the effective default value for this input variable,
// or cty.NilVal if this variable is required.
//
// If the configured default value is invalid, this returns a placeholder
// unknown value of the correct type. Use
// [InputVariableConfig.ValidateDefaultValue] instead if you are intending
// to report configuration diagnostics to the user.
func (v *InputVariableConfig) DefaultValue(ctx context.Context) cty.Value {
ret, _ := v.ValidateDefaultValue(ctx)
return ret
}
// ValidateDefaultValue verifies that the specified default value is valid
// and then returns the validated value. If the result is cty.NilVal then
// this input variable is required and so has no default value.
//
// If the returned diagnostics has errors then the returned value is a
// placeholder unknown value of the correct type.
func (v *InputVariableConfig) ValidateDefaultValue(ctx context.Context) (cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
val := v.config.DefaultValue
if val == cty.NilVal {
return cty.NilVal, diags
}
want := v.TypeConstraint()
if defs := v.NestedDefaults(); defs != nil {
val = defs.Apply(val)
}
val, err := convert.Convert(val, want)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid default value for input variable",
Detail: fmt.Sprintf("The default value does not conform to the variable's type constraint: %s.", err),
// TODO: Better to indicate the default value itself, but
// stackconfig.InputVariable doesn't currently retain that.
Subject: v.config.DeclRange.ToHCL().Ptr(),
})
return cty.UnknownVal(want), diags
}
return val, diags
}
// StackConfig returns the stack configuration that this input variable belongs
// to.
func (v *InputVariableConfig) StackConfig(ctx context.Context) *StackConfig {
return v.main.mustStackConfig(ctx, v.Addr().Stack)
}
// StackCallConfig returns the stack call that would be providing the value
// for this input variable, or nil if this input variable belongs to the
// main (root) stack and therefore its value would come from outside of
// the configuration.
func (v *InputVariableConfig) StackCallConfig(ctx context.Context) *StackCallConfig {
if v.Addr().Stack.IsRoot() {
return nil
}
stackConfigAddr := v.Addr().Stack.Parent()
caller := v.main.mustStackConfig(ctx, stackConfigAddr)
return caller.StackCall(ctx, stackaddrs.StackCall{Name: stackConfigAddr[len(stackConfigAddr)-1].Name})
}
// ExprReferenceValue implements Referenceable
func (v *InputVariableConfig) ExprReferenceValue(ctx context.Context, phase EvalPhase) cty.Value {
if v.Addr().Stack.IsRoot() {
// During validation the root input variable values are always unknown,
// because validation tests whether the configuration is valid for
// _any_ inputs, rather than for _specific_ inputs.
return cty.UnknownVal(v.TypeConstraint())
} else {
// Our apparent value is the value assigned in the definition object
// in the parent call.
call := v.StackCallConfig(ctx)
val := call.InputVariableValues(ctx)[v.Addr().Item]
if val == cty.NilVal {
val = cty.UnknownVal(v.TypeConstraint())
}
return val
}
}
// Validate implements Validatable
func (v *InputVariableConfig) Validate(ctx context.Context) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
diags = diags.Append(
v.ValidateDefaultValue(ctx),
)
return diags
}

@ -0,0 +1,86 @@
package stackeval
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
)
// Main is the central node of all data required for performing the major
// actions against a stack: validation, planning, and applying.
//
// This type delegates to various other types in this package to implement
// the real logic, with Main focused on enabling the collaboration between
// objects of those other types.
type Main struct {
config *stackconfig.Config
// planning captures the data needed when creating or applying a plan,
// but which need not be populated when only using the validation-related
// functionality of this package.
planning *mainPlanning
// The remaining fields memoize other objects we might create in response
// to method calls. Must lock "mu" before interacting with them.
mu sync.Mutex
mainStackConfig *StackConfig
}
type mainPlanning struct {
opts PlanOpts
}
type mainApplying struct {
opts ApplyOpts
}
// MainStackConfig returns the [StackConfig] object representing the main
// stack, which is at the root of the configuration tree.
func (m *Main) MainStackConfig(ctx context.Context) *StackConfig {
m.mu.Lock()
defer m.mu.Unlock()
if m.mainStackConfig == nil {
m.mainStackConfig = newStackConfig(m, stackaddrs.RootStack, m.config.Root)
}
return m.mainStackConfig
}
// StackConfig returns the [StackConfig] object representing the stack with
// the given address, or nil if there is no such stack.
func (m *Main) StackConfig(ctx context.Context, addr stackaddrs.Stack) *StackConfig {
ret := m.MainStackConfig(ctx)
for _, step := range addr {
ret = ret.ChildConfig(ctx, step)
if ret == nil {
return nil
}
}
return ret
}
// mustStackConfig is like [Main.StackConfig] except that it panics if it
// does not find a stack configuration object matching the given address,
// for situations where the absense of a stack config represents a bug
// somewhere in Terraform, rather than incorrect user input.
func (m *Main) mustStackConfig(ctx context.Context, addr stackaddrs.Stack) *StackConfig {
ret := m.StackConfig(ctx, addr)
if ret == nil {
panic(fmt.Sprintf("no configuration for %s", addr))
}
return ret
}
// StackCallConfig returns the [StackCallConfig] object representing the
// "stack" block in the configuration with the given address, or nil if there
// is no such block.
func (m *Main) StackCallConfig(ctx context.Context, addr stackaddrs.ConfigStackCall) *StackCallConfig {
caller := m.StackConfig(ctx, addr.Stack)
if caller == nil {
return nil
}
return caller.StackCall(ctx, addr.Item)
}

@ -0,0 +1,12 @@
package stackeval
import (
"github.com/hashicorp/terraform/internal/plans"
"github.com/zclconf/go-cty/cty"
)
type PlanOpts struct {
PlanningMode plans.Mode
InputVariableValues map[string]cty.Value
}

@ -0,0 +1,34 @@
package stackeval
import (
"context"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// Stack represents an instance of a [StackConfig] after it's had its
// repetition arguments (if any) evaluated to determine how many instances
// it has.
type Stack struct {
addr stackaddrs.StackInstance
main *Main
}
var _ ExpressionScope = (*Stack)(nil)
func (s *Stack) Addr() stackaddrs.StackInstance {
return s.addr
}
func (s *Stack) IsRoot() bool {
return s.addr.IsRoot()
}
// ResolveExpressionReference implements ExpressionScope, providing the
// global scope for evaluation within an already-instanciated stack during the
// plan and apply phases.
func (s *Stack) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) {
panic("unimplemented")
}

@ -0,0 +1,17 @@
package stackeval
import (
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
)
// StackCall represents a "stack" block in a stack configuration after
// its containing stacks have been expanded into stack instances.
type StackCall struct {
addr stackaddrs.AbsStackCall
main *Main
}
func (c *StackCall) Addr() stackaddrs.AbsStackCall {
return c.addr
}

@ -0,0 +1,201 @@
package stackeval
import (
"context"
"fmt"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/promising"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/tfdiags"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
)
// StackCallConfig represents a "stack" block in a stack configuration,
// representing a call to an embedded stack.
type StackCallConfig struct {
addr stackaddrs.ConfigStackCall
config *stackconfig.EmbeddedStack
main *Main
inputVariableValues promising.Once[withDiagnostics[map[stackaddrs.InputVariable]cty.Value]]
}
var _ Validatable = (*InputVariableConfig)(nil)
var _ ExpressionScope = (*StackCallConfig)(nil)
func newStackCallConfig(main *Main, addr stackaddrs.ConfigStackCall, config *stackconfig.EmbeddedStack) *StackCallConfig {
return &StackCallConfig{
addr: addr,
config: config,
main: main,
}
}
func (s *StackCallConfig) Addr() stackaddrs.ConfigStackCall {
return s.addr
}
// CallerConfig returns the object representing the stack configuration that this
// stack call was declared within.
func (s *StackCallConfig) CallerConfig(ctx context.Context) *StackConfig {
return s.main.mustStackConfig(ctx, s.Addr().Stack)
}
// CalleeConfig returns the object representing the child stack configuration
// that this stack call is referring to.
func (s *StackCallConfig) CalleeConfig(ctx context.Context) *StackConfig {
return s.main.mustStackConfig(ctx, s.Addr().Stack.Child(s.addr.Item.Name))
}
// ValidateInputVariableValues evaluates the "inputs" argument inside the
// configuration block, ensure that it's valid per the expectations of the
// child stack config, and then returns the resulting values.
//
// A [StackCallConfig] represents the not-yet-expanded stack call, so the
// result is an approximation of the input variables for all instances of
// this call. To get the final values for a particular instance, use
// [StackCall.InputVariableValues] instead.
//
// If the returned diagnostics contains errors then the returned values may
// be incomplete, but should at least be of the types specified in the
// variable declarations.
func (s *StackCallConfig) ValidateInputVariableValues(ctx context.Context) (map[stackaddrs.InputVariable]cty.Value, tfdiags.Diagnostics) {
// FIXME: This once-handling is pretty awkward when there are diagnostics
// involved. Can we find a better way to handle this common situation?
ret, err := s.inputVariableValues.Do(ctx, func(ctx context.Context) (withDiagnostics[map[stackaddrs.InputVariable]cty.Value], error) {
ret, diags := func() (map[stackaddrs.InputVariable]cty.Value, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
callee := s.CalleeConfig(ctx)
vars := callee.InputVariables(ctx)
atys := make(map[string]cty.Type, len(vars))
var optional []string
defs := make(map[string]cty.Value, len(vars))
for addr, v := range vars {
aty := v.TypeConstraint()
atys[addr.Name] = aty
if def := v.DefaultValue(ctx); def != cty.NilVal {
optional = append(optional, addr.Name)
defs[addr.Name] = def
}
}
oty := cty.ObjectWithOptionalAttrs(atys, optional)
varsObj, moreDiags := EvalExpr(ctx, s.config.Inputs, ValidatePhase, s)
diags = diags.Append(moreDiags)
if moreDiags.HasErrors() {
varsObj = cty.UnknownVal(oty.WithoutOptionalAttributesDeep())
}
// FIXME: TODO: We need to apply the nested optional attribute defaults
// somewhere in here too, but it isn't clear where we should do that since
// we're supposed to do that before type conversion but we don't yet have
// the isolated variable values to apply the defaults to.
varsObj, err := convert.Convert(varsObj, oty)
if err != nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid input variable definitions",
Detail: fmt.Sprintf(
"Unsuitable input variable definitions: %s.",
tfdiags.FormatError(err),
),
Subject: s.config.Inputs.Range().Ptr(),
})
varsObj = cty.UnknownVal(oty.WithoutOptionalAttributesDeep())
}
ret := make(map[stackaddrs.InputVariable]cty.Value, len(vars))
for addr := range vars {
val := varsObj.GetAttr(addr.Name)
if val.IsNull() {
if def, ok := defs[addr.Name]; ok {
ret[addr] = def
} else {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Missing definition for required input variable",
Detail: fmt.Sprintf("The input variable %q is required, so cannot be omitted.", addr.Name),
Subject: s.config.Inputs.Range().Ptr(),
})
ret[addr] = cty.UnknownVal(atys[addr.Name])
}
} else {
ret[addr] = val
}
}
return ret, diags
}()
return withDiagnostics[map[stackaddrs.InputVariable]cty.Value]{
Result: ret,
Diagnostics: diags,
}, nil
})
if err != nil {
// TODO: A better error message for promise resolution failures.
ret.Diagnostics = ret.Diagnostics.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Failed to evaluate input variable definitions",
Detail: fmt.Sprintf("Could not evaluate the input variable definitions for this call: %s.", err),
Subject: s.config.DeclRange.ToHCL().Ptr(),
})
}
return ret.Result, ret.Diagnostics
}
// InputVariableValues returns the effective input variable values specified
// in this call, or correctly-typed placeholders if any values are invalid.
//
// This is intended to support downstream evaluation of other objects during
// the validate phase, rather than for direct validation of this object. If you
// are intending to report problems directly to the user, use
// [StackCallConfig.ValidateInputVariableValues] instead.
func (s *StackCallConfig) InputVariableValues(ctx context.Context) map[stackaddrs.InputVariable]cty.Value {
ret, _ := s.ValidateInputVariableValues(ctx)
return ret
}
// ResolveExpressionReference implements ExpressionScope for evaluating
// expressions within a "stack" block during the validation phase.
//
// Note that the "stack" block lives in the caller scope rather than the
// callee scope, so this scope is not appropriate for evaluating anything
// inside the child variable declarations: they belong to the callee
// scope.
//
// This scope produces an approximation of expression results that is true
// for all instances of the stack call, not final results for a specific
// instance of a stack call. This is not the right scope to use during the
// plan and apply phases.
func (s *StackCallConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) {
repetition := instances.RepetitionData{}
if s.config.ForEach != nil {
// We're producing an approximation across all eventual instances
// of this call, so we'll set each.key and each.value to unknown
// values.
repetition.EachKey = cty.UnknownVal(cty.String).RefineNotNull()
repetition.EachValue = cty.DynamicVal
}
return s.main.
mustStackConfig(ctx, s.Addr().Stack).
resolveExpressionReference(ctx, ref, instances.RepetitionData{}, nil)
}
// Validate implements Validatable
func (s *StackCallConfig) Validate(ctx context.Context) tfdiags.Diagnostics {
var diags tfdiags.Diagnostics
diags = diags.Append(
s.ValidateInputVariableValues(ctx),
)
return diags
}

@ -0,0 +1,181 @@
package stackeval
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/terraform/internal/instances"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
"github.com/hashicorp/terraform/internal/stacks/stackconfig"
"github.com/hashicorp/terraform/internal/tfdiags"
)
// StackConfig represents a stack as represented in the configuration: either the
// root stack or one of the embedded stacks before it's been expanded into
// individual instances.
//
// After instance expansion we use [StackInstance] to represent each of the
// individual instances.
type StackConfig struct {
addr stackaddrs.Stack
config *stackconfig.ConfigNode
main *Main
// The remaining fields are where we memoize related objects that we've
// constructed and returned. Must lock "mu" before interacting with these.
mu sync.Mutex
children map[stackaddrs.StackStep]*StackConfig
inputVariables map[stackaddrs.InputVariable]*InputVariableConfig
stackCalls map[stackaddrs.StackCall]*StackCallConfig
}
var _ ExpressionScope = (*StackConfig)(nil)
func newStackConfig(main *Main, addr stackaddrs.Stack, config *stackconfig.ConfigNode) *StackConfig {
return &StackConfig{
addr: addr,
config: config,
main: main,
}
}
func (s *StackConfig) Addr() stackaddrs.Stack {
return s.addr
}
func (s *StackConfig) IsRoot() bool {
return s.addr.IsRoot()
}
// ParentAddr returns the address of the containing stack, or panics if called
// on the root stack (since it has no parent).
func (s *StackConfig) ParentAddr() stackaddrs.Stack {
return s.addr.Parent()
}
// ParentConfig returns the [StackConfig] object representing the configuration
// of the containing stack, or nil if the receiver is the root stack in the
// tree.
func (s *StackConfig) ParentConfig(ctx context.Context) *StackConfig {
if s.IsRoot() {
return nil
}
// Each StackConfig is only responsible for looking after its direct
// children, so to navigate upwards we need to start back at the
// root and work our way through the tree again.
return s.main.StackConfig(ctx, s.ParentAddr())
}
// ChildConfig returns a [StackConfig] representing the embedded stack matching
// the given address step, or nil if there is no such stack.
func (s *StackConfig) ChildConfig(ctx context.Context, step stackaddrs.StackStep) *StackConfig {
s.mu.Lock()
defer s.mu.Unlock()
ret, ok := s.children[step]
if !ok {
childNode, ok := s.config.Children[step.Name]
if !ok {
return nil
}
childAddr := s.Addr().Child(step.Name)
s.children[step] = newStackConfig(s.main, childAddr, childNode)
ret = s.children[step]
}
return ret
}
// InputVariable returns an [InputVariableConfig] representing the input
// variable declared within this stack config that matches the given
// address, or nil if there is no such declaration.
func (s *StackConfig) InputVariable(ctx context.Context, addr stackaddrs.InputVariable) *InputVariableConfig {
s.mu.Lock()
defer s.mu.Unlock()
ret, ok := s.inputVariables[addr]
if !ok {
cfg, ok := s.config.Stack.InputVariables[addr.Name]
if !ok {
return nil
}
cfgAddr := stackaddrs.Config(s.Addr(), addr)
s.inputVariables[addr] = newInputVariableConfig(s.main, cfgAddr, cfg)
}
return ret
}
// InputVariables returns a map of the objects representing all of the
// input variables declared inside this stack configuration.
func (s *StackConfig) InputVariables(ctx context.Context) map[stackaddrs.InputVariable]*InputVariableConfig {
if len(s.config.Stack.InputVariables) == 0 {
return nil
}
ret := make(map[stackaddrs.InputVariable]*InputVariableConfig, len(s.config.Stack.InputVariables))
for name := range s.config.Stack.InputVariables {
addr := stackaddrs.InputVariable{Name: name}
ret[addr] = s.InputVariable(ctx, addr)
}
return ret
}
// StackCall returns a [StackCallConfig] representing the "stack" block
// matching the given address declared within this stack config, or nil if
// there is no such declaration.
func (s *StackConfig) StackCall(ctx context.Context, addr stackaddrs.StackCall) *StackCallConfig {
s.mu.Lock()
defer s.mu.Unlock()
ret, ok := s.stackCalls[addr]
if !ok {
cfg, ok := s.config.Stack.EmbeddedStacks[addr.Name]
if !ok {
return nil
}
cfgAddr := stackaddrs.Config(s.Addr(), addr)
s.stackCalls[addr] = newStackCallConfig(s.main, cfgAddr, cfg)
}
return ret
}
// ResolveExpressionReference implements ExpressionScope, providing the
// global scope for evaluation within an unexpanded stack during the validate
// phase.
func (s *StackConfig) ResolveExpressionReference(ctx context.Context, ref stackaddrs.Reference) (Referenceable, tfdiags.Diagnostics) {
return s.resolveExpressionReference(ctx, ref, instances.RepetitionData{}, nil)
}
// resolveExpressionReference is the shared implementation of various
// validation-time ResolveExpressionReference methods, factoring out all
// of the common parts into one place.
func (s *StackConfig) resolveExpressionReference(ctx context.Context, ref stackaddrs.Reference, repetition instances.RepetitionData, selfAddr stackaddrs.Referenceable) (Referenceable, tfdiags.Diagnostics) {
var diags tfdiags.Diagnostics
// TODO: Most of the below would benefit from "Did you mean..." suggestions
// when something is missing but there's a similarly-named object nearby.
switch addr := ref.Target.(type) {
case stackaddrs.InputVariable:
ret := s.InputVariable(ctx, addr)
if ret == nil {
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Reference to undefined input variable",
Detail: fmt.Sprintf("There is no variable %q block in this stack.", addr.Name),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
}
return ret, diags
default:
diags = diags.Append(&hcl.Diagnostic{
Severity: hcl.DiagError,
Summary: "Invalid reference",
Detail: fmt.Sprintf("The object %s is not in scope at this location.", addr.String()),
Subject: ref.SourceRange.ToHCL().Ptr(),
})
return nil, diags
}
}

@ -0,0 +1,29 @@
package stackeval
import (
"context"
"github.com/hashicorp/terraform/internal/tfdiags"
)
type ValidateOpts struct {
}
// Validateable is implemented by objects that can participate in validation.
type Validatable interface {
// Validate returns diagnostics for any part of the reciever which
// has an invalid configuration.
//
// Validate implementations should be shallow, which is to say that
// in particular they _must not_ call the Validate method of other
// objects that implement Validatable, and should also think very
// hard about calling any validation-related methods of other objects,
// so as to avoid generating duplicate diagnostics via two different
// return paths.
//
// In general, assume that _all_ objects that implement Validatable will
// have their Validate methods called at some point during validation, and
// so it's unnecessary and harmful to try to handle validation on behalf of
// some other related object.
Validate(ctx context.Context) tfdiags.Diagnostics
}
Loading…
Cancel
Save