2023-09-27 19:59:48 -04:00
// Copyright IBM Corp. 2014, 2026
// SPDX-License-Identifier: BUSL-1.1
2023-05-24 13:12:19 -04:00
package stackconfig
import (
"fmt"
"github.com/hashicorp/hcl/v2"
2024-09-05 06:22:16 -04:00
2023-05-24 13:12:19 -04:00
"github.com/hashicorp/terraform/internal/addrs"
2025-04-03 04:29:18 -04:00
"github.com/hashicorp/terraform/internal/collections"
"github.com/hashicorp/terraform/internal/stacks/stackaddrs"
2023-05-24 13:12:19 -04:00
"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
2024-09-05 06:22:16 -04:00
2025-04-04 09:01:37 -04:00
// 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.
2025-04-16 08:05:51 -04:00
RemovedEmbeddedStacks collections . Map [ stackaddrs . ConfigStackCall , [ ] * Removed ]
2023-05-24 13:12:19 -04:00
}
func makeDeclarations ( ) Declarations {
return Declarations {
2025-04-04 09:01:37 -04:00
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 ] ( ) ,
2025-04-16 08:05:51 -04:00
RemovedEmbeddedStacks : collections . NewMap [ stackaddrs . ConfigStackCall , [ ] * Removed ] ( ) ,
2023-05-24 13:12:19 -04:00
}
}
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
}
2025-04-04 09:01:37 -04:00
if blocks , exists := d . RemovedComponents . GetOk ( stackaddrs . ConfigComponent {
2025-04-03 04:29:18 -04:00
Stack : nil ,
Item : stackaddrs . Component {
Name : name ,
} ,
} ) ; exists {
2025-03-18 03:54:27 -04:00
for _ , removed := range blocks {
2025-04-03 04:29:18 -04:00
if removed . From . Component . Index == nil {
2025-03-18 03:54:27 -04:00
// 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
}
}
2024-09-05 06:22:16 -04:00
}
2023-05-24 13:12:19 -04:00
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
}
2025-04-16 08:05:51 -04:00
if blocks , exists := d . RemovedEmbeddedStacks . GetOk ( stackaddrs . ConfigStackCall {
Stack : nil ,
Item : stackaddrs . StackCall {
Name : name ,
} ,
2025-04-04 09:01:37 -04:00
} ) ; 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
}
}
}
2023-05-24 13:12:19 -04:00
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
}
2024-09-05 06:22:16 -04:00
func ( d * Declarations ) addRemoved ( decl * Removed ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
if decl == nil {
return diags
}
2025-04-04 09:01:37 -04:00
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
}
2024-09-05 06:22:16 -04:00
}
2025-04-04 09:01:37 -04:00
d . RemovedComponents . Put ( addr , append ( d . RemovedComponents . Get ( addr ) , decl ) )
} else {
2025-04-16 08:05:51 -04:00
addr := decl . From . TargetStack ( ) . ToStackCall ( )
2025-04-04 09:01:37 -04:00
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 ) )
2024-09-05 06:22:16 -04:00
}
return diags
}
2023-05-24 13:12:19 -04:00
func ( d * Declarations ) merge ( other * Declarations ) tfdiags . Diagnostics {
var diags tfdiags . Diagnostics
for _ , decl := range other . EmbeddedStacks {
diags = diags . Append (
d . addEmbeddedStack ( decl ) ,
)
}
2025-04-04 09:01:37 -04:00
for _ , blocks := range other . RemovedEmbeddedStacks . All ( ) {
for _ , decl := range blocks {
diags = diags . Append ( d . addRemoved ( decl ) )
}
}
2023-05-24 13:12:19 -04:00
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 ) ,
)
}
2025-04-04 09:01:37 -04:00
for _ , blocks := range other . RemovedComponents . All ( ) {
2025-03-18 03:54:27 -04:00
for _ , decl := range blocks {
diags = diags . Append (
d . addRemoved ( decl ) ,
)
}
2024-09-05 06:22:16 -04:00
}
2023-05-24 13:12:19 -04:00
return diags
}