| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package stackconfig |
| |
| import ( |
| "fmt" |
| "sort" |
| "strings" |
| |
| "github.com/apparentlymart/go-versions/versions" |
| "github.com/apparentlymart/go-versions/versions/constraints" |
| "github.com/hashicorp/go-slug/sourceaddrs" |
| "github.com/hashicorp/go-slug/sourcebundle" |
| "github.com/hashicorp/hcl/v2" |
| "github.com/zclconf/go-cty/cty" |
| |
| "github.com/hashicorp/terraform/internal/addrs" |
| "github.com/hashicorp/terraform/internal/stacks/stackaddrs" |
| "github.com/hashicorp/terraform/internal/stacks/stackconfig/stackconfigtypes" |
| "github.com/hashicorp/terraform/internal/stacks/stackconfig/typeexpr" |
| "github.com/hashicorp/terraform/internal/tfdiags" |
| ) |
| |
| // maxEmbeddedStackNesting is an arbitrary, hopefully-reasonable limit on |
| // how much embedded stack nesting is allowed in a stack configuration. |
| // |
| // This is here to avoid unbounded resource usage for configurations with |
| // mistakes such as self-referencing source addresses or call cycles. |
| const maxEmbeddedStackNesting = 20 |
| |
| // Config represents an overall stack configuration tree, consisting of a |
| // root stack that might optionally have embedded stacks inside it, and |
| // so on for arbitrary levels of nesting. |
| type Config struct { |
| Root *ConfigNode |
| |
| // Sources is the source bundle that the configuration was loaded from. |
| // |
| // This is also the source bundle that any Terraform modules used by |
| // components should be loaded from. |
| Sources *sourcebundle.Bundle |
| |
| // ProviderRefTypes tracks the cty capsule type that represents a |
| // reference for each provider type mentioned in the configuration. |
| ProviderRefTypes map[addrs.Provider]cty.Type |
| } |
| |
| func (config *Config) Stack(stack stackaddrs.Stack) *Stack { |
| current := config.Root |
| for _, part := range stack { |
| var ok bool |
| current, ok = current.Children[part.Name] |
| if !ok { |
| return nil |
| } |
| } |
| return current.Stack |
| } |
| |
| func (config *Config) Component(component stackaddrs.ConfigComponent) *Component { |
| stack := config.Stack(component.Stack) |
| if stack == nil || stack.Components == nil { |
| return nil |
| } |
| return stack.Components[component.Item.Name] |
| } |
| |
| // ConfigNode represents a node in a tree of stacks that are to be planned and |
| // applied together. |
| // |
| // A fully-resolved stack configuration has a root node of this type, which |
| // can have zero or more child nodes that are also of this type, and so on |
| // to arbitrary levels of nesting. |
| type ConfigNode struct { |
| // Stack is the definition of this node in the stack tree. |
| Stack *Stack |
| |
| // Source is the source address of this stack. This is mainly used to |
| // ensure consistency in places where a stack might be initialised in |
| // multiple places (like in different source blocks). |
| Source sourceaddrs.FinalSource |
| |
| // Children describes all of the embedded stacks nested directly beneath |
| // this node in the stack tree. The keys match the labels on the "stack" |
| // blocks in the configuration that [Config.Stack] was built from, and |
| // so also match the keys in the EmbeddedStacks field of that Stack. |
| Children map[string]*ConfigNode |
| } |
| |
| // LoadConfigDir loads, parses, decodes, and partially-validates the |
| // stack configuration rooted at the given source address. |
| // |
| // If the given source address is a [sourceaddrs.LocalSource] then it is |
| // interpreted relative to the current process working directory. If it's |
| // a remote our registry source address then LoadConfigDir will attempt |
| // to read it from the provided source bundle. |
| // |
| // LoadConfigDir follows calls to embedded stacks and recursively loads |
| // those too, using the same source bundle for any non-local sources. |
| func LoadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle) (*Config, tfdiags.Diagnostics) { |
| rootNode, diags := loadConfigDir(sourceAddr, sources, make([]sourceaddrs.FinalSource, 0, 3)) |
| if rootNode == nil { |
| if !diags.HasErrors() { |
| panic("LoadConfigDir returned no root node and no errors") |
| } |
| return nil, diags |
| } |
| |
| ret := &Config{ |
| Root: rootNode, |
| Sources: sources, |
| } |
| |
| // Before we return we need to walk the tree and find all of the mentions |
| // of provider types and make sure we have a singleton cty.Type for each |
| // one representing a reference to a configuration of each type. |
| providerRefTypes, moreDiags := collectProviderRefCapsuleTypes(ret) |
| ret.ProviderRefTypes = providerRefTypes |
| diags = diags.Append(moreDiags) |
| |
| return ret, diags |
| } |
| |
| // NewEmptyConfig returns a representation of an empty configuration that's |
| // primarily intended for unit testing situations that don't actually depend |
| // on any configuration objects being present. |
| // |
| // The result has non-nil pointers to some items that callers would reasonably |
| // expect should always be present, but in particular doesn't include any |
| // actual declarations and so closely resembles what would happen if |
| // parsing a totally-empty configuration. |
| func NewEmptyConfig(fakeSourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle) *Config { |
| return &Config{ |
| Root: &ConfigNode{ |
| Stack: &Stack{ |
| SourceAddr: fakeSourceAddr, |
| Declarations: Declarations{ |
| RequiredProviders: &ProviderRequirements{}, |
| }, |
| }, |
| }, |
| Sources: sources, |
| } |
| } |
| |
| func loadConfigDir(sourceAddr sourceaddrs.FinalSource, sources *sourcebundle.Bundle, callers []sourceaddrs.FinalSource) (*ConfigNode, tfdiags.Diagnostics) { |
| stack, diags := LoadSingleStackConfig(sourceAddr, sources) |
| if stack == nil { |
| if !diags.HasErrors() { |
| panic("LoadSingleStackConfig returned no root node and no errors") |
| } |
| return nil, diags |
| } |
| |
| ret := &ConfigNode{ |
| Stack: stack, |
| Source: sourceAddr, |
| Children: make(map[string]*ConfigNode), |
| } |
| for _, call := range stack.EmbeddedStacks { |
| effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, call.SourceAddr, call.VersionConstraints, sources) |
| if err != nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid source address", |
| Detail: fmt.Sprintf( |
| "Cannot use %q as a source address here: %s.", |
| call.SourceAddr, err, |
| ), |
| Subject: call.SourceAddrRange.ToHCL().Ptr(), |
| }) |
| continue |
| } |
| call.FinalSourceAddr = effectiveSourceAddr |
| |
| if len(callers) == maxEmbeddedStackNesting { |
| var callersBuf strings.Builder |
| for i, addr := range callers { |
| fmt.Fprintf(&callersBuf, "\n %2d: %s", i+1, addr) |
| } |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Too much embedded stack nesting", |
| Detail: fmt.Sprintf( |
| "This embedded stack call is nested %d levels deep, which is greater than Terraform's nesting safety limit.\n\nWe recommend keeping stack configuration trees relatively flat, ideally using composition of a flat set of nested calls at the root.\n\nEmbedded stacks leading to this point:%s", |
| len(callers), callersBuf.String(), |
| ), |
| Subject: call.DeclRange.ToHCL().Ptr(), |
| }) |
| continue |
| } |
| |
| childNode, moreDiags := loadConfigDir(effectiveSourceAddr, sources, append(callers, sourceAddr)) |
| diags = diags.Append(moreDiags) |
| if childNode != nil { |
| ret.Children[call.Name] = childNode |
| } |
| } |
| |
| var removedTargets []stackaddrs.ConfigStackCall |
| for target := range stack.RemovedEmbeddedStacks.All() { |
| // removed embedded stacks can point to deeply embedded stacks, |
| // which we actually want to load into the embedded stacks if they |
| // naturally exist. But, the parents of those deeply embedded stacks |
| // will only exist if we have already added their parents to the |
| // tree of objects. So, we're going to store all our removed blocks |
| // in a flattened list and sort them so we add children before |
| // grandchildren and onwards and properly build the list to place |
| // everything in the correct place. |
| removedTargets = append(removedTargets, target) |
| } |
| |
| sort.Slice(removedTargets, func(i, j int) bool { |
| return len(removedTargets[i].Stack) < len(removedTargets[j].Stack) |
| }) |
| |
| for _, target := range removedTargets { |
| for _, block := range stack.RemovedEmbeddedStacks.Get(target) { |
| effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, block.SourceAddr, block.VersionConstraints, sources) |
| if err != nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid source address", |
| Detail: fmt.Sprintf( |
| "Cannot use %q as a source address here: %s.", |
| block.SourceAddr, err, |
| ), |
| Subject: block.SourceAddrRange.ToHCL().Ptr(), |
| }) |
| continue |
| } |
| block.FinalSourceAddr = effectiveSourceAddr |
| |
| current := ret |
| for _, step := range target.Stack { |
| current = current.Children[step.Name] |
| if current == nil { |
| // this is invalid, we can't have orphaned removed blocks |
| // so we'll just return an error and skip this block. |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid removed block", |
| Detail: "The linked removed block was not executed because the `from` attribute of the removed block targets a component or embedded stack within an orphaned embedded stack.\n\nIn order to remove an entire stack, update your removed block to target the entire removed stack itself instead of the specific elements within it.", |
| Subject: block.DeclRange.ToHCL().Ptr(), |
| }) |
| break |
| } |
| } |
| |
| if current != nil { |
| next := target.Item.Name |
| if childNode, ok := current.Children[next]; ok { |
| // Then we've already loaded the configuration for this |
| // stack in the direct stack call or in another removed |
| // block. |
| |
| if childNode.Source != block.FinalSourceAddr { |
| // but apparently the blocks don't agree on what the |
| // source should be here, so that is an error |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid source address", |
| Detail: fmt.Sprintf("Cannot use %q as a source address here: the target stack is already initialised with another source %q.", block.FinalSourceAddr, childNode.Source), |
| Subject: block.SourceAddrRange.ToHCL().Ptr(), |
| }) |
| } |
| continue |
| } |
| |
| childNode, moreDiags := loadConfigDir(effectiveSourceAddr, sources, append(callers, sourceAddr)) |
| diags = diags.Append(moreDiags) |
| if childNode != nil { |
| current.Children[next] = childNode |
| } |
| } |
| } |
| } |
| |
| // We'll also populate the FinalSourceAddr field on each component, |
| // so that callers can know the final absolute address of this |
| // component's root module without having to retrace through our |
| // recursive process here. |
| for _, cmpn := range stack.Components { |
| effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, cmpn.SourceAddr, cmpn.VersionConstraints, sources) |
| if err != nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid source address", |
| Detail: fmt.Sprintf( |
| "Cannot use %q as a source address here: %s.", |
| cmpn.SourceAddr, err, |
| ), |
| Subject: cmpn.SourceAddrRange.ToHCL().Ptr(), |
| }) |
| continue |
| } |
| |
| cmpn.FinalSourceAddr = effectiveSourceAddr |
| } |
| |
| for addr, blocks := range stack.RemovedComponents.All() { |
| |
| var source sourceaddrs.FinalSource |
| if len(addr.Stack) == 0 { |
| if cmpn, ok := stack.Components[addr.Item.Name]; ok { |
| source = cmpn.FinalSourceAddr |
| } |
| } |
| |
| for _, rmvd := range blocks { |
| effectiveSourceAddr, err := resolveFinalSourceAddr(sourceAddr, rmvd.SourceAddr, rmvd.VersionConstraints, sources) |
| if err != nil { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid source address", |
| Detail: fmt.Sprintf( |
| "Cannot use %q as a source address here: %s.", |
| rmvd.SourceAddr, err, |
| ), |
| Subject: rmvd.SourceAddrRange.ToHCL().Ptr(), |
| }) |
| continue |
| } |
| |
| if source == nil { |
| source = effectiveSourceAddr |
| } else if source != effectiveSourceAddr { |
| diags = diags.Append(&hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid source address", |
| Detail: fmt.Sprintf("Cannot use %q as a source address here: the target stack is already initialised with another source %q.", effectiveSourceAddr, source), |
| Subject: rmvd.SourceAddrRange.ToHCL().Ptr(), |
| }) |
| } |
| |
| rmvd.FinalSourceAddr = effectiveSourceAddr |
| } |
| } |
| |
| return ret, diags |
| } |
| |
| func resolveFinalSourceAddr(base sourceaddrs.FinalSource, rel sourceaddrs.Source, versionConstraints constraints.IntersectionSpec, sources *sourcebundle.Bundle) (sourceaddrs.FinalSource, error) { |
| switch rel := rel.(type) { |
| case sourceaddrs.FinalSource: |
| switch base := base.(type) { |
| case sourceaddrs.RegistrySourceFinal: |
| // This case is awkward because we'd ideally like to return |
| // another registry source address in the same registry package |
| // as base, but that might not actually be possible if "rel" |
| // is a local source that traverses up out of the scope of |
| // the registry package and into other parts of the real |
| // underlying package. Therefore we'll first try the ideal |
| // case but then do some more complex finagling if it fails. |
| ret, err := sourceaddrs.ResolveRelativeFinalSource(base, rel) |
| if err == nil { |
| return ret, nil |
| } |
| |
| // If we can't resolve relative to the registry source then |
| // we need to resolve relative to its underlying remote source |
| // instead. |
| underlyingSource, ok := sources.RegistryPackageSourceAddr(base.Package(), base.SelectedVersion()) |
| if !ok { |
| // If we also can't find the underlying source for some reason |
| // then we're stuck. |
| return nil, fmt.Errorf("can't find underlying source address for %s", base.Package()) |
| } |
| underlyingSource = base.FinalSourceAddr(underlyingSource) |
| return sourceaddrs.ResolveRelativeFinalSource(underlyingSource, rel) |
| |
| default: |
| // Easy case: this source type is already a final type |
| return sourceaddrs.ResolveRelativeFinalSource(base, rel) |
| } |
| case sourceaddrs.RegistrySource: |
| // Registry sources are more annoying because we need to figure out |
| // exactly which version the given version constraints select, which |
| // we infer by what's available in the source bundle on the assumption |
| // that the source bundler also selected the latest available version |
| // that meets the given constraints. |
| allowedVersions := versions.MeetingConstraints(versionConstraints) |
| availableVersions := sources.RegistryPackageVersions(rel.Package()) |
| selectedVersion := availableVersions.NewestInSet(allowedVersions) |
| if selectedVersion == versions.Unspecified { |
| // We should get here only if the source bundle was built |
| // incorrectly. A valid source bundle should always contain |
| // at least one entry that matches each version constraint. |
| return nil, fmt.Errorf("no cached versions of %s match the given version constraints", rel.Package()) |
| } |
| finalRel := rel.Versioned(selectedVersion) |
| return sourceaddrs.ResolveRelativeFinalSource(base, finalRel) |
| default: |
| // Should not get here because the above cases should be exhaustive |
| // for all implementations of sourceaddrs.Source. |
| return nil, fmt.Errorf("cannot resolve final source address for %T (this is a bug in Terraform)", rel) |
| } |
| } |
| |
| // collectProviderRefCapsuleTypes searches the entire configuration tree for |
| // any mentions of provider types and instantiates the singleton cty capsule |
| // type representing configurations for each one, returning a mapping from |
| // provider source address to type. |
| // |
| // This operation involves some further analysis of some configuration elements |
| // which can potentially produce additional diagnostics. |
| func collectProviderRefCapsuleTypes(config *Config) (map[addrs.Provider]cty.Type, tfdiags.Diagnostics) { |
| var diags tfdiags.Diagnostics |
| ret := make(map[addrs.Provider]cty.Type) |
| |
| // Our main source for provider references is required_providers blocks |
| // in each of the individual stack configurations. This should be |
| // exhaustive for a valid configuration because we require that all |
| // provider requirements be declared before use elsewhere. |
| collectProviderRefCapsuleTypesSingle(config.Root, ret) |
| |
| // Type constraints in input variables and output values can include |
| // provider reference types. This makes sure we'll have capsule types |
| // for each one and also, as a side-effect, updates the input variable |
| // and output value objects to refer to those type constraints for |
| // use in later evaluation. (In practice this should not discover any |
| // new provider types in a valid configuration, but populating extra |
| // fields on the InputVariable and OutputValue objects is an important |
| // side-effect.) |
| diags = diags.Append( |
| decodeTypeConstraints(config, ret), |
| ) |
| |
| return ret, diags |
| } |
| |
| func collectProviderRefCapsuleTypesSingle(node *ConfigNode, types map[addrs.Provider]cty.Type) { |
| reqs := node.Stack.RequiredProviders |
| if reqs == nil { |
| return |
| } |
| for _, req := range reqs.Requirements { |
| pTy := req.Provider |
| if _, ok := types[pTy]; ok { |
| continue |
| } |
| types[pTy] = stackconfigtypes.ProviderConfigType(pTy) |
| } |
| |
| for _, child := range node.Children { |
| collectProviderRefCapsuleTypesSingle(child, types) |
| } |
| } |
| |
| // decodeTypeConstraints handles the just-in-time postprocessing we do before |
| // returning from [LoadConfigDir], making sure that the type constraints |
| // on input variables and output values throughout the configuration are |
| // valid and consistent. |
| func decodeTypeConstraints(config *Config, types map[addrs.Provider]cty.Type) tfdiags.Diagnostics { |
| return decodeTypeConstraintsSingle(config.Root, types) |
| } |
| |
| func decodeTypeConstraintsSingle(node *ConfigNode, types map[addrs.Provider]cty.Type) tfdiags.Diagnostics { |
| var diags tfdiags.Diagnostics |
| |
| typeInfo := &decodeTypeConstraintsTypeInfo{ |
| types: types, |
| reqs: node.Stack.RequiredProviders, |
| } |
| for _, c := range node.Stack.InputVariables { |
| diags = diags.Append( |
| decodeTypeConstraint(&c.Type, typeInfo), |
| ) |
| } |
| for _, c := range node.Stack.OutputValues { |
| diags = diags.Append( |
| decodeTypeConstraint(&c.Type, typeInfo), |
| ) |
| } |
| |
| for _, child := range node.Children { |
| diags = diags.Append( |
| decodeTypeConstraintsSingle(child, types), |
| ) |
| } |
| |
| return diags |
| } |
| |
| func decodeTypeConstraint(c *TypeConstraint, typeInfo *decodeTypeConstraintsTypeInfo) tfdiags.Diagnostics { |
| var diags tfdiags.Diagnostics |
| ty, defaults, hclDiags := typeexpr.TypeConstraint(c.Expression, typeInfo) |
| c.Constraint = ty |
| c.Defaults = defaults |
| diags = diags.Append(hclDiags) |
| return diags |
| } |
| |
| type decodeTypeConstraintsTypeInfo struct { |
| types map[addrs.Provider]cty.Type |
| reqs *ProviderRequirements |
| } |
| |
| var _ typeexpr.TypeInformation = (*decodeTypeConstraintsTypeInfo)(nil) |
| |
| // ProviderConfigType implements typeexpr.TypeInformation |
| func (ti *decodeTypeConstraintsTypeInfo) ProviderConfigType(providerAddr addrs.Provider) cty.Type { |
| return ti.types[providerAddr] |
| } |
| |
| // ProviderForLocalName implements typeexpr.TypeInformation |
| func (ti *decodeTypeConstraintsTypeInfo) ProviderForLocalName(localName string) (addrs.Provider, bool) { |
| if ti.reqs == nil { |
| return addrs.Provider{}, false |
| } |
| return ti.reqs.ProviderForLocalName(localName) |
| } |
| |
| // SetProviderConfigType implements typeexpr.TypeInformation |
| func (ti *decodeTypeConstraintsTypeInfo) SetProviderConfigType(providerAddr addrs.Provider, ty cty.Type) { |
| ti.types[providerAddr] = ty |
| } |