From 3d503f8e71e91b58e4a5713f0817ee7ee315bf68 Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Fri, 26 May 2023 12:03:47 -0700 Subject: [PATCH] stackruntime: Stubbing the "interpreter" for stack configurations --- internal/stacks/stackaddrs/component.go | 4 + internal/stacks/stackaddrs/in_stack.go | 10 + internal/stacks/stackaddrs/input_variable.go | 4 + internal/stacks/stackaddrs/local_value.go | 4 + internal/stacks/stackaddrs/output_value.go | 4 + internal/stacks/stackaddrs/provider_config.go | 4 + internal/stacks/stackaddrs/reference.go | 89 ++++++++ internal/stacks/stackaddrs/referenceable.go | 1 + internal/stacks/stackaddrs/stack.go | 12 ++ internal/stacks/stackaddrs/stack_call.go | 4 + internal/stacks/stackruntime/doc.go | 4 + .../internal/stackeval/applying.go | 3 + .../internal/stackeval/diagnostics.go | 8 + .../stackruntime/internal/stackeval/doc.go | 13 ++ .../internal/stackeval/evalphase_string.go | 37 ++++ .../internal/stackeval/expressions.go | 132 ++++++++++++ .../internal/stackeval/input_variable.go | 16 ++ .../stackeval/input_variable_config.go | 135 ++++++++++++ .../stackruntime/internal/stackeval/main.go | 86 ++++++++ .../internal/stackeval/planning.go | 12 ++ .../stackruntime/internal/stackeval/stack.go | 34 +++ .../internal/stackeval/stack_call.go | 17 ++ .../internal/stackeval/stack_call_config.go | 201 ++++++++++++++++++ .../internal/stackeval/stack_config.go | 181 ++++++++++++++++ .../internal/stackeval/validating.go | 29 +++ 25 files changed, 1044 insertions(+) create mode 100644 internal/stacks/stackaddrs/reference.go create mode 100644 internal/stacks/stackruntime/doc.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/applying.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/diagnostics.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/doc.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/evalphase_string.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/expressions.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/input_variable.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/input_variable_config.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/main.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/planning.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/stack.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/stack_call.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/stack_call_config.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/stack_config.go create mode 100644 internal/stacks/stackruntime/internal/stackeval/validating.go diff --git a/internal/stacks/stackaddrs/component.go b/internal/stacks/stackaddrs/component.go index 3d5e8c5d6c..2003820169 100644 --- a/internal/stacks/stackaddrs/component.go +++ b/internal/stacks/stackaddrs/component.go @@ -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] diff --git a/internal/stacks/stackaddrs/in_stack.go b/internal/stacks/stackaddrs/in_stack.go index de3ff05754..2966413ffd 100644 --- a/internal/stacks/stackaddrs/in_stack.go +++ b/internal/stacks/stackaddrs/in_stack.go @@ -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) +} diff --git a/internal/stacks/stackaddrs/input_variable.go b/internal/stacks/stackaddrs/input_variable.go index e4752fcd48..c789b68792 100644 --- a/internal/stacks/stackaddrs/input_variable.go +++ b/internal/stacks/stackaddrs/input_variable.go @@ -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] diff --git a/internal/stacks/stackaddrs/local_value.go b/internal/stacks/stackaddrs/local_value.go index 1d6b23def1..8d747954cb 100644 --- a/internal/stacks/stackaddrs/local_value.go +++ b/internal/stacks/stackaddrs/local_value.go @@ -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] diff --git a/internal/stacks/stackaddrs/output_value.go b/internal/stacks/stackaddrs/output_value.go index db9fe93d35..7d90b41ddd 100644 --- a/internal/stacks/stackaddrs/output_value.go +++ b/internal/stacks/stackaddrs/output_value.go @@ -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] diff --git a/internal/stacks/stackaddrs/provider_config.go b/internal/stacks/stackaddrs/provider_config.go index bb3de4e633..c2c86b55b1 100644 --- a/internal/stacks/stackaddrs/provider_config.go +++ b/internal/stacks/stackaddrs/provider_config.go @@ -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 diff --git a/internal/stacks/stackaddrs/reference.go b/internal/stacks/stackaddrs/reference.go new file mode 100644 index 0000000000..4e581493b7 --- /dev/null +++ b/internal/stacks/stackaddrs/reference.go @@ -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 +} diff --git a/internal/stacks/stackaddrs/referenceable.go b/internal/stacks/stackaddrs/referenceable.go index 8636e024ab..b6fcb8247f 100644 --- a/internal/stacks/stackaddrs/referenceable.go +++ b/internal/stacks/stackaddrs/referenceable.go @@ -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{} diff --git a/internal/stacks/stackaddrs/stack.go b/internal/stacks/stackaddrs/stack.go index 9eaeb0de56..22ad175465 100644 --- a/internal/stacks/stackaddrs/stack.go +++ b/internal/stacks/stackaddrs/stack.go @@ -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 +} diff --git a/internal/stacks/stackaddrs/stack_call.go b/internal/stacks/stackaddrs/stack_call.go index 92d24a4f3a..5329dffa77 100644 --- a/internal/stacks/stackaddrs/stack_call.go +++ b/internal/stacks/stackaddrs/stack_call.go @@ -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] diff --git a/internal/stacks/stackruntime/doc.go b/internal/stacks/stackruntime/doc.go new file mode 100644 index 0000000000..4a976e31c8 --- /dev/null +++ b/internal/stacks/stackruntime/doc.go @@ -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 diff --git a/internal/stacks/stackruntime/internal/stackeval/applying.go b/internal/stacks/stackruntime/internal/stackeval/applying.go new file mode 100644 index 0000000000..4e3057a767 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/applying.go @@ -0,0 +1,3 @@ +package stackeval + +type ApplyOpts struct{} diff --git a/internal/stacks/stackruntime/internal/stackeval/diagnostics.go b/internal/stacks/stackruntime/internal/stackeval/diagnostics.go new file mode 100644 index 0000000000..ea1de82ecd --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/diagnostics.go @@ -0,0 +1,8 @@ +package stackeval + +import "github.com/hashicorp/terraform/internal/tfdiags" + +type withDiagnostics[T any] struct { + Result T + Diagnostics tfdiags.Diagnostics +} diff --git a/internal/stacks/stackruntime/internal/stackeval/doc.go b/internal/stacks/stackruntime/internal/stackeval/doc.go new file mode 100644 index 0000000000..e7782818af --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/doc.go @@ -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 diff --git a/internal/stacks/stackruntime/internal/stackeval/evalphase_string.go b/internal/stacks/stackruntime/internal/stackeval/evalphase_string.go new file mode 100644 index 0000000000..fcac94e84b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/evalphase_string.go @@ -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) + ")" + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/expressions.go b/internal/stacks/stackruntime/internal/stackeval/expressions.go new file mode 100644 index 0000000000..eaa91ef512 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/expressions.go @@ -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 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable.go b/internal/stacks/stackruntime/internal/stackeval/input_variable.go new file mode 100644 index 0000000000..061919955b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable.go @@ -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 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/input_variable_config.go b/internal/stacks/stackruntime/internal/stackeval/input_variable_config.go new file mode 100644 index 0000000000..37b38ebc30 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/input_variable_config.go @@ -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 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/main.go b/internal/stacks/stackruntime/internal/stackeval/main.go new file mode 100644 index 0000000000..d67683690d --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/main.go @@ -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) +} diff --git a/internal/stacks/stackruntime/internal/stackeval/planning.go b/internal/stacks/stackruntime/internal/stackeval/planning.go new file mode 100644 index 0000000000..694fc2aa25 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/planning.go @@ -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 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack.go b/internal/stacks/stackruntime/internal/stackeval/stack.go new file mode 100644 index 0000000000..4051b7fd5e --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack.go @@ -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") +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call.go b/internal/stacks/stackruntime/internal/stackeval/stack_call.go new file mode 100644 index 0000000000..7f3db5c7ef --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call.go @@ -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 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go new file mode 100644 index 0000000000..8cf2682a04 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_call_config.go @@ -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 +} diff --git a/internal/stacks/stackruntime/internal/stackeval/stack_config.go b/internal/stacks/stackruntime/internal/stackeval/stack_config.go new file mode 100644 index 0000000000..52e7f2f663 --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/stack_config.go @@ -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 + } +} diff --git a/internal/stacks/stackruntime/internal/stackeval/validating.go b/internal/stacks/stackruntime/internal/stackeval/validating.go new file mode 100644 index 0000000000..b6e880e38b --- /dev/null +++ b/internal/stacks/stackruntime/internal/stackeval/validating.go @@ -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 +}