From 30020bca55694385c130bc605a5fe18661fa2311 Mon Sep 17 00:00:00 2001 From: Kazuma Watanabe Date: Mon, 23 Sep 2024 16:19:45 +0000 Subject: [PATCH] Override "terraform" blocks See https://developer.hashicorp.com/terraform/language/files/override#merging-terraform-blocks The settings within terraform blocks are considered individually when merging. If the required_providers argument is set, its value is merged on an element-by-element basis, which allows an override block to adjust the constraint for a single provider without affecting the constraints for other providers. In both the required_version and required_providers settings, each override constraint entirely replaces the constraints for the same component in the original block. If both the base block and the override block both set required_version then the constraints in the base block are entirely ignored. The presence of a block defining a backend (either cloud or backend) in an override file always takes precedence over a block defining a backend in the original configuration. That is, if a cloud block is set within the original configuration and a backend block is set in the override file, Terraform will use the backend block specified in the override file upon merging. Similarly, if a backend block is set within the original configuration and a cloud block is set in the override file, Terraform will use the cloud block specified in the override file upon merging. --- terraform/module.go | 132 +++++++++-- terraform/module_test.go | 477 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 587 insertions(+), 22 deletions(-) diff --git a/terraform/module.go b/terraform/module.go index 9a32e03d5..bf6b1e8a1 100644 --- a/terraform/module.go +++ b/terraform/module.go @@ -169,40 +169,128 @@ func (m *Module) PartialContent(schema *hclext.BodySchema, ctx *Evaluator) (*hcl // Note that this function returns the overwritten primary blocks // but has side effects on the primary blocks. func overrideBlocks(primaries, overrides hclext.Blocks) hclext.Blocks { - dict := map[string]*hclext.Block{} + dict := map[string]hclext.Blocks{} for _, primary := range primaries { - // A top-level block in an override file merges with a block in a normal configuration file - // that has the same block header. - // The block header is the block type and any quoted labels that follow it. - key := fmt.Sprintf("%s[%s]", primary.Type, strings.Join(primary.Labels, ",")) - dict[key] = primary + switch primary.Type { + case "terraform": + // The "terraform" blocks are allowed to be declared multiple times. + dict[primary.Type] = append(dict[primary.Type], primary) + + default: + // A top-level block in an override file merges with a block in a normal configuration file + // that has the same block header. + // The block header is the block type and any quoted labels that follow it. + key := fmt.Sprintf("%s[%s]", primary.Type, strings.Join(primary.Labels, ",")) + dict[key] = hclext.Blocks{primary} + } } + newPrimaries := hclext.Blocks{} for _, override := range overrides { - key := fmt.Sprintf("%s[%s]", override.Type, strings.Join(override.Labels, ",")) - if primary, exists := dict[key]; exists { - // Within a top-level block, an attribute argument within an override block - // replaces any argument of the same name in the original block. - for name, attr := range override.Body.Attributes { - primary.Body.Attributes[name] = attr + switch override.Type { + case "terraform": + // Any required_providers that were not used for overrides will be added, + // so we will track whether they were used for overrides or not. + overrideRequiredProviders := override.Body.Blocks.ByType()["required_providers"] + + for _, primary := range dict[override.Type] { + // In both the required_version and required_providers settings, + // each override constraint entirely replaces the constraints for + // the same component in the original block. + for name, attr := range override.Body.Attributes { + primary.Body.Attributes[name] = attr + } + + for _, overrideInnerBlock := range override.Body.Blocks { + switch overrideInnerBlock.Type { + case "required_providers": + // If the required_providers argument is set, its value is merged on an element-by-element basis + for _, primaryInnerBlock := range primary.Body.Blocks { + if primaryInnerBlock.Type == "required_providers" { + for name, attr := range overrideInnerBlock.Body.Attributes { + if _, exists := primaryInnerBlock.Body.Attributes[name]; exists { + primaryInnerBlock.Body.Attributes[name] = attr + // Remove the required provider that was used to override. + for _, requiredProvider := range overrideRequiredProviders { + delete(requiredProvider.Body.Attributes, name) + } + } + } + } + } + + case "cloud", "backend": + // The presence of a block defining a backend (either cloud or backend) in an override file + // always takes precedence over a block defining a backend in the original configuration. + newInnerBlocks := hclext.Blocks{} + for _, primaryInnerBlock := range primary.Body.Blocks { + if primaryInnerBlock.Type != "cloud" && primaryInnerBlock.Type != "backend" { + newInnerBlocks = append(newInnerBlocks, primaryInnerBlock) + } + } + primary.Body.Blocks = append(newInnerBlocks, overrideInnerBlock) + + default: + newInnerBlocks := hclext.Blocks{} + for _, primaryInnerBlock := range primary.Body.Blocks { + if primaryInnerBlock.Type != overrideInnerBlock.Type { + newInnerBlocks = append(newInnerBlocks, primaryInnerBlock) + } + } + primary.Body.Blocks = append(newInnerBlocks, overrideInnerBlock) + } + } + } + + // Any remaining required providers that aren't overridden will be added as a new block. + newRequiredProviders := hclext.Blocks{} + for _, requiredProvider := range overrideRequiredProviders { + if len(requiredProvider.Body.Attributes) > 0 { + newRequiredProviders = append(newRequiredProviders, requiredProvider) + } } + if len(newRequiredProviders) > 0 { + newPrimaries = append(newPrimaries, &hclext.Block{ + Type: override.Type, + Labels: override.Labels, + Body: &hclext.BodyContent{ + Blocks: newRequiredProviders, + }, + DefRange: override.DefRange, + TypeRange: override.TypeRange, + LabelRanges: override.LabelRanges, + }) + } + + default: + key := fmt.Sprintf("%s[%s]", override.Type, strings.Join(override.Labels, ",")) + if primaries, exists := dict[key]; exists { + // The general rule, duplicated blocks are not allowed. + primary := primaries[0] + + // Within a top-level block, an attribute argument within an override block + // replaces any argument of the same name in the original block. + for name, attr := range override.Body.Attributes { + primary.Body.Attributes[name] = attr + } - // Within a top-level block, any nested blocks within an override block replace - // all blocks of the same type in the original block. - // Any block types that do not appear in the override block remain from the original block. - for _, overrideBlock := range override.Body.Blocks { - overriddenBlocks := hclext.Blocks{} - for _, primaryBlock := range primary.Body.Blocks { - if primaryBlock.Type != overrideBlock.Type { - overriddenBlocks = append(overriddenBlocks, primaryBlock) + // Within a top-level block, any nested blocks within an override block replace + // all blocks of the same type in the original block. + // Any block types that do not appear in the override block remain from the original block. + for _, overrideInnerBlock := range override.Body.Blocks { + newInnerBlocks := hclext.Blocks{} + for _, primaryInnerBlock := range primary.Body.Blocks { + if primaryInnerBlock.Type != overrideInnerBlock.Type { + newInnerBlocks = append(newInnerBlocks, primaryInnerBlock) + } } + primary.Body.Blocks = append(newInnerBlocks, overrideInnerBlock) } - primary.Body.Blocks = append(overriddenBlocks, overrideBlock) } } } - return primaries + return append(primaries, newPrimaries...) } var moduleSchema = &hclext.BodySchema{ diff --git a/terraform/module_test.go b/terraform/module_test.go index f532abf87..10f5ea3ed 100644 --- a/terraform/module_test.go +++ b/terraform/module_test.go @@ -806,6 +806,483 @@ func Test_overrideBlocks(t *testing.T) { }, }, }, + { + Name: "no override multiple required_version", + Primaries: hclext.Blocks{ + // The "terraform" blocks are allowed to be declared multiple times. + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version1"}}, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version2"}}, + }, + }, + }, + Overrides: hclext.Blocks{}, + Want: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version1"}}, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version2"}}, + }, + }, + }, + }, + { + Name: "override multiple required_version", + Primaries: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version1"}}, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version2"}}, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version3"}}, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version4"}}, + }, + }, + }, + Want: hclext.Blocks{ + // In both the required_version and required_providers settings, + // each override constraint entirely replaces the constraints for the same component in the original block. + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version4"}}, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"required_version": &hclext.Attribute{Name: "required_version4"}}, + }, + }, + }, + }, + { + Name: "no override required_providers", + Primaries: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "aws": &hclext.Attribute{Name: "aws"}, + "google": &hclext.Attribute{Name: "google"}, + }, + }, + }, + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "azurerm": &hclext.Attribute{Name: "azurerm"}, + }, + }, + }, + }, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "google-beta": &hclext.Attribute{Name: "google-beta"}, + }, + }, + }, + }, + }, + }, + }, + Overrides: hclext.Blocks{}, + Want: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "aws": &hclext.Attribute{Name: "aws"}, + "google": &hclext.Attribute{Name: "google"}, + }, + }, + }, + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "azurerm": &hclext.Attribute{Name: "azurerm"}, + }, + }, + }, + }, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "google-beta": &hclext.Attribute{Name: "google-beta"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + // If the required_providers argument is set, its value is merged on an element-by-element basis + Name: "override required_providers", + Primaries: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "aws": &hclext.Attribute{Name: "aws"}, + "google": &hclext.Attribute{Name: "google"}, + }, + }, + }, + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "azurerm": &hclext.Attribute{Name: "azurerm"}, + }, + }, + }, + }, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "google-beta": &hclext.Attribute{Name: "google-beta"}, + }, + }, + }, + }, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "aws": &hclext.Attribute{Name: "aws2"}, + "azurerm": &hclext.Attribute{Name: "azurerm2"}, + }, + }, + }, + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "assert": &hclext.Attribute{Name: "assert"}, + }, + }, + }, + }, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "google": &hclext.Attribute{Name: "google2"}, + "time": &hclext.Attribute{Name: "time"}, + }, + }, + }, + }, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "aws": &hclext.Attribute{Name: "aws2"}, + "google": &hclext.Attribute{Name: "google2"}, + }, + }, + }, + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "azurerm": &hclext.Attribute{Name: "azurerm2"}, + }, + }, + }, + }, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "google-beta": &hclext.Attribute{Name: "google-beta"}, + }, + }, + }, + }, + }, + }, + // Blocks not present in the primaries are added + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "assert": &hclext.Attribute{Name: "assert"}, + }, + }, + }, + }, + }, + }, + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "required_providers", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "time": &hclext.Attribute{Name: "time"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override backend", + Primaries: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "backend", + Labels: []string{"local"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"path": &hclext.Attribute{Name: "path"}}, + }, + }, + }, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "backend", + Labels: []string{"remote"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"host": &hclext.Attribute{Name: "host"}}, + }, + }, + }, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "backend", + Labels: []string{"remote"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"host": &hclext.Attribute{Name: "host"}}, + }, + }, + }, + }, + }, + }, + }, + { + // The presence of a block defining a backend (either cloud or backend) in an override file + // always takes precedence over a block defining a backend in the original configuration + Name: "override backend by cloud", + Primaries: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "backend", + Labels: []string{"local"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"path": &hclext.Attribute{Name: "path"}}, + }, + }, + }, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "cloud", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"organization": &hclext.Attribute{Name: "organization"}}, + }, + }, + }, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "cloud", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"organization": &hclext.Attribute{Name: "organization"}}, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override cloud by backend", + Primaries: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "cloud", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"organization": &hclext.Attribute{Name: "organization"}}, + }, + }, + }, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "backend", + Labels: []string{"remote"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"host": &hclext.Attribute{Name: "host"}}, + }, + }, + }, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "terraform", + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "backend", + Labels: []string{"remote"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"host": &hclext.Attribute{Name: "host"}}, + }, + }, + }, + }, + }, + }, + }, } for _, test := range tests {