// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package stackconfig import ( "fmt" "github.com/hashicorp/hcl/v2" "github.com/hashicorp/terraform/internal/addrs" "github.com/hashicorp/terraform/internal/collections" "github.com/hashicorp/terraform/internal/stacks/stackaddrs" "github.com/hashicorp/terraform/internal/tfdiags" ) // Declarations represent the various items that can be declared in a stack // configuration. // // This just represents the fields that both [Stack] and [File] have in common, // so we can share some code between the two. type Declarations struct { // EmbeddedStacks are calls to other stack configurations that should // be treated as a part of the overall desired state produced from this // stack. These are declared with "stack" blocks in the stack language. EmbeddedStacks map[string]*EmbeddedStack // Components are calls to trees of Terraform modules that represent the // real infrastructure described by a stack. Components map[string]*Component // InputVariables, LocalValues, and OutputValues together represent all // of the "named values" in the stack configuration, which are just glue // to pass values between scopes or to factor out common expressions for // reuse in multiple locations. InputVariables map[string]*InputVariable LocalValues map[string]*LocalValue OutputValues map[string]*OutputValue // RequiredProviders represents the single required_providers block // that's allowed in any stack, declaring which providers this stack // depends on and which versions of those providers it is compatible with. RequiredProviders *ProviderRequirements // ProviderConfigs are the provider configurations declared in this // particular stack configuration. Other stack configurations in the // overall tree might have their own provider configurations. ProviderConfigs map[addrs.LocalProviderConfig]*ProviderConfig // RemovedComponents is the list of components that have been removed from // the configuration. RemovedComponents collections.Map[stackaddrs.ConfigComponent, []*Removed] // RemovedEmbeddedStacks is the list of embedded stacks that have been removed // from the configuration. RemovedEmbeddedStacks collections.Map[stackaddrs.ConfigStackCall, []*Removed] } func makeDeclarations() Declarations { return Declarations{ EmbeddedStacks: make(map[string]*EmbeddedStack), Components: make(map[string]*Component), InputVariables: make(map[string]*InputVariable), LocalValues: make(map[string]*LocalValue), OutputValues: make(map[string]*OutputValue), ProviderConfigs: make(map[addrs.LocalProviderConfig]*ProviderConfig), RemovedComponents: collections.NewMap[stackaddrs.ConfigComponent, []*Removed](), RemovedEmbeddedStacks: collections.NewMap[stackaddrs.ConfigStackCall, []*Removed](), } } func (d *Declarations) addComponent(decl *Component) tfdiags.Diagnostics { if decl == nil { return nil } var diags tfdiags.Diagnostics name := decl.Name if existing, exists := d.Components[name]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate component declaration", Detail: fmt.Sprintf( "An component named %q was already declared at %s.", name, existing.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } if blocks, exists := d.RemovedComponents.GetOk(stackaddrs.ConfigComponent{ Stack: nil, Item: stackaddrs.Component{ Name: name, }, }); exists { for _, removed := range blocks { if removed.From.Component.Index == nil { // If a component has been removed, we should not also find it // in the configuration. // // If the removed block has an index, then it's possible that // only a specific instance was removed and not the whole thing. // This is okay at this point, and will be validated more later. // See the addRemoved method for more information. diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Component exists for removed block", Detail: fmt.Sprintf( "A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.", name, decl.DeclRange.ToHCL(), ), Subject: removed.DeclRange.ToHCL().Ptr(), }) return diags } } } d.Components[name] = decl return diags } func (d *Declarations) addEmbeddedStack(decl *EmbeddedStack) tfdiags.Diagnostics { if decl == nil { return nil } var diags tfdiags.Diagnostics name := decl.Name if existing, exists := d.EmbeddedStacks[name]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate embedded stack call", Detail: fmt.Sprintf( "An embedded stack call named %q was already declared at %s.", name, existing.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } if blocks, exists := d.RemovedEmbeddedStacks.GetOk(stackaddrs.ConfigStackCall{ Stack: nil, Item: stackaddrs.StackCall{ Name: name, }, }); exists { for _, removed := range blocks { if removed.From.Stack[0].Index == nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Stack exists for removed block", Detail: fmt.Sprintf( "A removed block for stack %q was declared without an index, but a stack block with the same name was declared at %s.\n\nA removed block without an index indicates that the stack and all instances were removed from the configuration, and this is not the case.", name, decl.DeclRange.ToHCL(), ), Subject: removed.DeclRange.ToHCL().Ptr(), }) return diags } } } d.EmbeddedStacks[name] = decl return diags } func (d *Declarations) addInputVariable(decl *InputVariable) tfdiags.Diagnostics { if decl == nil { return nil } var diags tfdiags.Diagnostics name := decl.Name if existing, exists := d.InputVariables[name]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate input variable declaration", Detail: fmt.Sprintf( "An input variable named %q was already declared at %s.", name, existing.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } d.InputVariables[name] = decl return diags } func (d *Declarations) addLocalValue(decl *LocalValue) tfdiags.Diagnostics { if decl == nil { return nil } var diags tfdiags.Diagnostics name := decl.Name if existing, exists := d.LocalValues[name]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate local value declaration", Detail: fmt.Sprintf( "A local value named %q was already declared at %s.", name, existing.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } d.LocalValues[name] = decl return diags } func (d *Declarations) addOutputValue(decl *OutputValue) tfdiags.Diagnostics { if decl == nil { return nil } var diags tfdiags.Diagnostics name := decl.Name if existing, exists := d.OutputValues[name]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate output value declaration", Detail: fmt.Sprintf( "An output value named %q was already declared at %s.", name, existing.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } d.OutputValues[name] = decl return diags } func (d *Declarations) addRequiredProviders(decl *ProviderRequirements) tfdiags.Diagnostics { if decl == nil { return nil } var diags tfdiags.Diagnostics if d.RequiredProviders != nil { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate provider requirements", Detail: fmt.Sprintf( "This stack's provider requirements were already declared at %s.", d.RequiredProviders.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } d.RequiredProviders = decl return diags } func (d *Declarations) addProviderConfig(decl *ProviderConfig) tfdiags.Diagnostics { if decl == nil { return nil } var diags tfdiags.Diagnostics addr := decl.LocalAddr if existing, exists := d.ProviderConfigs[addr]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Duplicate provider configuration", Detail: fmt.Sprintf( "An configuration named %q for provider %q was already declared at %s.", addr.LocalName, addr.Alias, existing.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } d.ProviderConfigs[addr] = decl return diags } func (d *Declarations) addRemoved(decl *Removed) tfdiags.Diagnostics { var diags tfdiags.Diagnostics if decl == nil { return diags } if decl.From.Component != nil { addr := decl.From.TargetConfigComponent() if decl.From.Component.Index == nil && len(decl.From.Stack) == 0 { // If the removed block does not have an index, then we shouldn't also // have a component block with the same name. A removed block without // an index indicates that the component and all instances were removed // from the configuration. // // Note that a removed block with an index is allowed to coexist with a // component block with the same name, because it indicates that only // a specific instance was removed and not the whole thing. During the // validate and planning stages we will validate that the clashing // component and removed blocks are not both pointing to the same index. if component, exists := d.Components[decl.From.Component.Name]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Component exists for removed block", Detail: fmt.Sprintf( "A removed block for component %q was declared without an index, but a component block with the same name was declared at %s.\n\nA removed block without an index indicates that the component and all instances were removed from the configuration, and this is not the case.", decl.From.Component.Name, component.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } } d.RemovedComponents.Put(addr, append(d.RemovedComponents.Get(addr), decl)) } else { addr := decl.From.TargetStack().ToStackCall() if len(decl.From.Stack) == 1 && decl.From.Stack[0].Index == nil { // Same logic as for components, we can just error a bit earlier // here if the user is targeting a stack that definitely exists // in the configuration. if stack, exists := d.EmbeddedStacks[decl.From.Stack[0].Name]; exists { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Stack exists for removed block", Detail: fmt.Sprintf( "A removed block for stack %q was declared without an index, but a stack block with the same name was declared at %s.\n\nA removed block without an index indicates that the stack and all instances were removed from the configuration, and this is not the case.", decl.From.Component.Name, stack.DeclRange.ToHCL(), ), Subject: decl.DeclRange.ToHCL().Ptr(), }) return diags } } d.RemovedEmbeddedStacks.Put(addr, append(d.RemovedEmbeddedStacks.Get(addr), decl)) } return diags } func (d *Declarations) merge(other *Declarations) tfdiags.Diagnostics { var diags tfdiags.Diagnostics for _, decl := range other.EmbeddedStacks { diags = diags.Append( d.addEmbeddedStack(decl), ) } for _, blocks := range other.RemovedEmbeddedStacks.All() { for _, decl := range blocks { diags = diags.Append(d.addRemoved(decl)) } } for _, decl := range other.Components { diags = diags.Append( d.addComponent(decl), ) } for _, decl := range other.InputVariables { diags = diags.Append( d.addInputVariable(decl), ) } for _, decl := range other.LocalValues { diags = diags.Append( d.addLocalValue(decl), ) } for _, decl := range other.OutputValues { diags = diags.Append( d.addOutputValue(decl), ) } if other.RequiredProviders != nil { d.addRequiredProviders(other.RequiredProviders) } for _, decl := range other.ProviderConfigs { diags = diags.Append( d.addProviderConfig(decl), ) } for _, blocks := range other.RemovedComponents.All() { for _, decl := range blocks { diags = diags.Append( d.addRemoved(decl), ) } } return diags }