| // Copyright (c) HashiCorp, Inc. |
| // SPDX-License-Identifier: BUSL-1.1 |
| |
| package stackconfig |
| |
| import ( |
| "fmt" |
| "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 |
| |
| // 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, |
| 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 |
| } |
| |
| 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 |
| } |
| } |
| |
| // 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 _, blocks := range stack.Removed { |
| 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 |
| } |
| |
| 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 |
| } |