mirror of https://github.com/hashicorp/terraform
parent
41d931d21a
commit
3d503f8e71
@ -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
|
||||
}
|
||||
@ -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…
Reference in new issue