| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package stackeval |
| |
| import ( |
| "context" |
| "fmt" |
| "sync" |
| "time" |
| |
| "github.com/hashicorp/hcl/v2" |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/instances" |
| "github.com/hashicorp/terraform/internal/lang" |
| "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 |
| parent *StackConfig |
| |
| 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 |
| localValues map[stackaddrs.LocalValue]*LocalValueConfig |
| outputValues map[stackaddrs.OutputValue]*OutputValueConfig |
| stackCalls map[stackaddrs.StackCall]*StackCallConfig |
| components map[stackaddrs.Component]*ComponentConfig |
| removed map[stackaddrs.Component][]*RemovedComponentConfig |
| providers map[stackaddrs.ProviderConfig]*ProviderConfig |
| } |
| |
| var ( |
| _ ExpressionScope = (*StackConfig)(nil) |
| ) |
| |
| func newStackConfig(main *Main, addr stackaddrs.Stack, parent *StackConfig, config *stackconfig.ConfigNode) *StackConfig { |
| return &StackConfig{ |
| addr: addr, |
| parent: parent, |
| config: config, |
| main: main, |
| |
| children: make(map[stackaddrs.StackStep]*StackConfig, len(config.Children)), |
| inputVariables: make(map[stackaddrs.InputVariable]*InputVariableConfig, len(config.Stack.Declarations.InputVariables)), |
| localValues: make(map[stackaddrs.LocalValue]*LocalValueConfig, len(config.Stack.Declarations.LocalValues)), |
| outputValues: make(map[stackaddrs.OutputValue]*OutputValueConfig, len(config.Stack.Declarations.OutputValues)), |
| stackCalls: make(map[stackaddrs.StackCall]*StackCallConfig, len(config.Stack.Declarations.EmbeddedStacks)), |
| components: make(map[stackaddrs.Component]*ComponentConfig, len(config.Stack.Declarations.Components)), |
| removed: make(map[stackaddrs.Component][]*RemovedComponentConfig, len(config.Stack.Declarations.Removed)), |
| providers: make(map[stackaddrs.ProviderConfig]*ProviderConfig, len(config.Stack.Declarations.ProviderConfigs)), |
| } |
| } |
| |
| // 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(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, s, childNode) |
| ret = s.children[step] |
| } |
| return ret |
| } |
| |
| func (s *StackConfig) ChildConfigs() map[stackaddrs.StackStep]*StackConfig { |
| if len(s.config.Children) == 0 { |
| return nil |
| } |
| ret := make(map[stackaddrs.StackStep]*StackConfig, len(s.config.Children)) |
| for n := range s.config.Children { |
| stepAddr := stackaddrs.StackStep{Name: n} |
| ret[stepAddr] = s.ChildConfig(stepAddr) |
| } |
| return ret |
| } |
| |
| // InputVariables returns a map of the objects representing all of the |
| // input variables declared inside this stack configuration. |
| func (s *StackConfig) InputVariables() 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(addr) |
| } |
| 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(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) |
| ret = newInputVariableConfig(s.main, cfgAddr, s, cfg) |
| s.inputVariables[addr] = ret |
| } |
| return ret |
| } |
| |
| // LocalValues returns a map of the objects representing all of the |
| // local values declared inside this stack configuration. |
| func (s *StackConfig) LocalValues() map[stackaddrs.LocalValue]*LocalValueConfig { |
| if len(s.config.Stack.LocalValues) == 0 { |
| return nil |
| } |
| ret := make(map[stackaddrs.LocalValue]*LocalValueConfig, len(s.config.Stack.LocalValues)) |
| for name := range s.config.Stack.LocalValues { |
| addr := stackaddrs.LocalValue{Name: name} |
| ret[addr] = s.LocalValue(addr) |
| } |
| return ret |
| } |
| |
| // LocalValue returns an [LocalValueConfig] 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) LocalValue(addr stackaddrs.LocalValue) *LocalValueConfig { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| ret, ok := s.localValues[addr] |
| if !ok { |
| cfg, ok := s.config.Stack.LocalValues[addr.Name] |
| if !ok { |
| return nil |
| } |
| cfgAddr := stackaddrs.Config(s.addr, addr) |
| ret = newLocalValueConfig(s.main, cfgAddr, s, cfg) |
| s.localValues[addr] = ret |
| } |
| return ret |
| } |
| |
| // OutputValue returns an [OutputValueConfig] representing the output |
| // value declared within this stack config that matches the given |
| // address, or nil if there is no such declaration. |
| func (s *StackConfig) OutputValue(addr stackaddrs.OutputValue) *OutputValueConfig { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| ret, ok := s.outputValues[addr] |
| if !ok { |
| cfg, ok := s.config.Stack.OutputValues[addr.Name] |
| if !ok { |
| return nil |
| } |
| cfgAddr := stackaddrs.Config(s.addr, addr) |
| ret = newOutputValueConfig(s.main, cfgAddr, s, cfg) |
| s.outputValues[addr] = ret |
| } |
| return ret |
| } |
| |
| // OutputValues returns a map of the objects representing all of the |
| // output values declared inside this stack configuration. |
| func (s *StackConfig) OutputValues() map[stackaddrs.OutputValue]*OutputValueConfig { |
| if len(s.config.Stack.OutputValues) == 0 { |
| return nil |
| } |
| ret := make(map[stackaddrs.OutputValue]*OutputValueConfig, len(s.config.Stack.OutputValues)) |
| for name := range s.config.Stack.OutputValues { |
| addr := stackaddrs.OutputValue{Name: name} |
| ret[addr] = s.OutputValue(addr) |
| } |
| return ret |
| } |
| |
| // ResultType returns the type of the result object that will be produced |
| // by this stack configuration, based on the output values declared within |
| // it. |
| func (s *StackConfig) ResultType() cty.Type { |
| os := s.OutputValues() |
| atys := make(map[string]cty.Type, len(os)) |
| for addr, o := range os { |
| atys[addr.Name] = o.ValueTypeConstraint() |
| } |
| return cty.Object(atys) |
| } |
| |
| // Providers returns a map of the objects representing all of the provider |
| // configurations declared inside this stack configuration. |
| func (s *StackConfig) Providers() map[stackaddrs.ProviderConfig]*ProviderConfig { |
| if len(s.config.Stack.ProviderConfigs) == 0 { |
| return nil |
| } |
| ret := make(map[stackaddrs.ProviderConfig]*ProviderConfig, len(s.config.Stack.ProviderConfigs)) |
| for configAddr := range s.config.Stack.ProviderConfigs { |
| provider, ok := s.config.Stack.RequiredProviders.ProviderForLocalName(configAddr.LocalName) |
| if !ok { |
| // Then we are missing a provider declaration, this will be caught |
| // elsewhere so we'll just skip it here. |
| continue |
| } |
| |
| addr := stackaddrs.ProviderConfig{ |
| Provider: provider, |
| Name: configAddr.Alias, |
| } |
| ret[addr] = s.Provider(addr) |
| } |
| return ret |
| } |
| |
| // Provider returns a [ProviderConfig] representing the provider configuration |
| // block within the stack configuration that matches the given address, |
| // or nil if there is no such declaration. |
| func (s *StackConfig) Provider(addr stackaddrs.ProviderConfig) *ProviderConfig { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| ret, ok := s.providers[addr] |
| if !ok { |
| localName, ok := s.config.Stack.RequiredProviders.LocalNameForProvider(addr.Provider) |
| if !ok { |
| return nil |
| } |
| // FIXME: stackconfig package currently uses addrs.LocalProviderConfig |
| // instead of stackaddrs.ProviderConfigRef. |
| configAddr := addrs.LocalProviderConfig{ |
| LocalName: localName, |
| Alias: addr.Name, |
| } |
| cfg, ok := s.config.Stack.ProviderConfigs[configAddr] |
| if !ok { |
| return nil |
| } |
| cfgAddr := stackaddrs.Config(s.addr, addr) |
| ret = newProviderConfig(s.main, cfgAddr, s, cfg) |
| s.providers[addr] = ret |
| } |
| return ret |
| } |
| |
| // ProviderByLocalAddr returns a [ProviderConfig] representing the provider |
| // configuration block within the stack configuration that matches the given |
| // local address, or nil if there is no such declaration. |
| // |
| // This is equivalent to calling [Provider] just using a reference address |
| // instead of a config address. |
| func (s *StackConfig) ProviderByLocalAddr(localAddr stackaddrs.ProviderConfigRef) *ProviderConfig { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| provider, ok := s.config.Stack.RequiredProviders.ProviderForLocalName(localAddr.ProviderLocalName) |
| if !ok { |
| return nil |
| } |
| |
| addr := stackaddrs.ProviderConfig{ |
| Provider: provider, |
| Name: localAddr.Name, |
| } |
| ret, ok := s.providers[addr] |
| if !ok { |
| configAddr := addrs.LocalProviderConfig{ |
| LocalName: localAddr.ProviderLocalName, |
| Alias: localAddr.Name, |
| } |
| cfg, ok := s.config.Stack.ProviderConfigs[configAddr] |
| if !ok { |
| return nil |
| } |
| cfgAddr := stackaddrs.Config(s.addr, addr) |
| ret = newProviderConfig(s.main, cfgAddr, s, cfg) |
| s.providers[addr] = ret |
| } |
| return ret |
| } |
| |
| // ProviderLocalName returns the local name used for the given provider |
| // in this particular stack configuration, based on the declarations in |
| // the required_providers configuration block. |
| // |
| // If the second return value is false then there is no local name declared |
| // for the given provider, and so the first return value is invalid. |
| func (s *StackConfig) ProviderLocalName(addr addrs.Provider) (string, bool) { |
| return s.config.Stack.RequiredProviders.LocalNameForProvider(addr) |
| } |
| |
| // ProviderForLocalName returns the provider for the given local name in this |
| // particular stack configuration, based on the declarations in the |
| // required_providers configuration block. |
| // |
| // If the second return value is false then there is no provider declared |
| // for the given local name, and so the first return value is invalid. |
| func (s *StackConfig) ProviderForLocalName(localName string) (addrs.Provider, bool) { |
| return s.config.Stack.RequiredProviders.ProviderForLocalName(localName) |
| } |
| |
| // 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(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) |
| ret = newStackCallConfig(s.main, cfgAddr, s, cfg) |
| s.stackCalls[addr] = ret |
| } |
| return ret |
| } |
| |
| // StackCalls returns a map of objects representing all of the embedded stack |
| // calls inside this stack configuration. |
| func (s *StackConfig) StackCalls() map[stackaddrs.StackCall]*StackCallConfig { |
| if len(s.config.Children) == 0 { |
| return nil |
| } |
| ret := make(map[stackaddrs.StackCall]*StackCallConfig, len(s.config.Children)) |
| for n := range s.config.Children { |
| stepAddr := stackaddrs.StackCall{Name: n} |
| ret[stepAddr] = s.StackCall(stepAddr) |
| } |
| return ret |
| } |
| |
| // Component returns a [ComponentConfig] representing the component call |
| // declared within this stack config that matches the given address, or nil if |
| // there is no such declaration. |
| func (s *StackConfig) Component(addr stackaddrs.Component) *ComponentConfig { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| ret, ok := s.components[addr] |
| if !ok { |
| cfg, ok := s.config.Stack.Components[addr.Name] |
| if !ok { |
| return nil |
| } |
| cfgAddr := stackaddrs.Config(s.addr, addr) |
| ret = newComponentConfig(s.main, cfgAddr, s, cfg) |
| s.components[addr] = ret |
| } |
| return ret |
| } |
| |
| // Components returns a map of the objects representing all of the |
| // component calls declared inside this stack configuration. |
| func (s *StackConfig) Components() map[stackaddrs.Component]*ComponentConfig { |
| if len(s.config.Stack.Components) == 0 { |
| return nil |
| } |
| ret := make(map[stackaddrs.Component]*ComponentConfig, len(s.config.Stack.Components)) |
| for name := range s.config.Stack.Components { |
| addr := stackaddrs.Component{Name: name} |
| ret[addr] = s.Component(addr) |
| } |
| return ret |
| } |
| |
| // RemovedComponent returns a [RemovedComponentConfig] representing the |
| // component call declared within this stack config that matches the given |
| // address, or nil if there is no such declaration. |
| func (s *StackConfig) RemovedComponent(addr stackaddrs.Component) []*RemovedComponentConfig { |
| s.mu.Lock() |
| defer s.mu.Unlock() |
| |
| ret, ok := s.removed[addr] |
| if !ok { |
| cfgs, ok := s.config.Stack.Removed[addr.Name] |
| if !ok { |
| return nil |
| } |
| for _, cfg := range cfgs { |
| cfgAddr := stackaddrs.Config(s.addr, addr) |
| removed := newRemovedComponentConfig(s.main, cfgAddr, s, cfg) |
| ret = append(ret, removed) |
| } |
| s.removed[addr] = ret |
| } |
| return ret |
| } |
| |
| // RemovedComponents returns a map of the objects representing all of the |
| // removed calls declared inside this stack configuration. |
| func (s *StackConfig) RemovedComponents() map[stackaddrs.Component][]*RemovedComponentConfig { |
| if len(s.config.Stack.Removed) == 0 { |
| return nil |
| } |
| ret := make(map[stackaddrs.Component][]*RemovedComponentConfig, len(s.config.Stack.Removed)) |
| for name := range s.config.Stack.Removed { |
| addr := stackaddrs.Component{Name: name} |
| ret[addr] = s.RemovedComponent(addr) |
| } |
| 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, nil, instances.RepetitionData{}) |
| } |
| |
| // 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, |
| selfAddr stackaddrs.Referenceable, |
| repetition instances.RepetitionData, |
| ) (Referenceable, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| |
| // "Test-only globals" is a special affordance we have only when running |
| // unit tests in this package. The function called in this branch will |
| // return an error itself if we're not running in a suitable test situation. |
| if addr, ok := ref.Target.(stackaddrs.TestOnlyGlobal); ok { |
| return s.main.resolveTestOnlyGlobalReference(addr, ref.SourceRange) |
| } |
| |
| // 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(addr) |
| if ret == nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Reference to undeclared input variable", |
| Detail: fmt.Sprintf("There is no variable %q block declared in this stack.", addr.Name), |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return ret, diags |
| case stackaddrs.LocalValue: |
| ret := s.LocalValue(addr) |
| if ret == nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Reference to undeclared local value", |
| Detail: fmt.Sprintf("There is no local %q declared in this stack.", addr.Name), |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return ret, diags |
| case stackaddrs.Component: |
| ret := s.Component(addr) |
| if ret == nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Reference to undeclared component", |
| Detail: fmt.Sprintf("There is no component %q block declared in this stack.", addr.Name), |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return ret, diags |
| case stackaddrs.StackCall: |
| ret := s.StackCall(addr) |
| if ret == nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Reference to undeclared embedded stack", |
| Detail: fmt.Sprintf("There is no stack %q block declared in this stack.", addr.Name), |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return ret, diags |
| case stackaddrs.ProviderConfigRef: |
| ret := s.ProviderByLocalAddr(addr) |
| if ret == nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Reference to undeclared provider configuration", |
| Detail: fmt.Sprintf("There is no provider %q %q block declared in this stack.", addr.ProviderLocalName, addr.Name), |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return ret, diags |
| case stackaddrs.ContextualRef: |
| switch addr { |
| case stackaddrs.EachKey: |
| if repetition.EachKey == cty.NilVal { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid 'each' reference", |
| Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return JustValue{repetition.EachKey}, diags |
| case stackaddrs.EachValue: |
| if repetition.EachValue == cty.NilVal { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid 'each' reference", |
| Detail: "The special symbol 'each' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'for_each' argument.", |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return JustValue{repetition.EachValue}, diags |
| case stackaddrs.CountIndex: |
| if repetition.CountIndex == cty.NilVal { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid 'count' reference", |
| Detail: "The special symbol 'count' is not defined in this location. This symbol is valid only inside multi-instance blocks that use the 'count' argument.", |
| Subject: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| return JustValue{repetition.CountIndex}, diags |
| case stackaddrs.Self: |
| if selfAddr != nil { |
| // We'll just pretend the reference was to whatever "self" |
| // is referring to, then. |
| ref.Target = selfAddr |
| return s.resolveExpressionReference(ctx, ref, nil, repetition) |
| } else { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid 'self' reference", |
| Detail: "The special symbol 'self' is not defined in this location.", |
| Context: ref.SourceRange.ToHCL().Ptr(), |
| }) |
| return nil, diags |
| } |
| default: |
| // The above should be exhaustive for all defined values of this type. |
| panic(fmt.Sprintf("unsupported ContextualRef %#v", addr)) |
| } |
| 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 |
| } |
| } |
| |
| // ExternalFunctions implements ExpressionScope. |
| func (s *StackConfig) ExternalFunctions(ctx context.Context) (lang.ExternalFuncs, tfdiags.Diagnostics) { |
| return s.main.ProviderFunctions(ctx, s) |
| } |
| |
| // PlanTimestamp implements ExpressionScope, providing the timestamp at which |
| // the current plan is being run. |
| func (s *StackConfig) PlanTimestamp() time.Time { |
| return s.main.PlanTimestamp() |
| } |