Skip to content

Commit

Permalink
Override exactly according to the Terraform spec (#2124)
Browse files Browse the repository at this point in the history
* Nested blocks should not be merged by overrides

See https://developer.hashicorp.com/terraform/language/files/override#merging-behavior

- 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.
- The contents of nested configuration blocks are not merged.

* Override lexicographical order

See https://developer.hashicorp.com/terraform/language/files/override#merging-behavior

If more than one override file defines the same top-level block, the overriding effect is compounded, with later blocks taking precedence over earlier blocks. Overrides are processed in order first by filename (in lexicographical order) and then by position in each file.

Regarding the position in each file, no additional considerations are necessary since hclext.Blocks are already sorted.

* 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.

* Override "locals" blocks

See also https://developer.hashicorp.com/terraform/language/files/override#merging-locals-blocks

Each locals block defines a number of named values. Overrides are applied on a value-by-value basis, ignoring which locals block they are defined in.

* Override "resource" blocks

See https://developer.hashicorp.com/terraform/language/files/override#merging-resource-and-data-blocks

Within a resource block, the contents of any lifecycle nested block are merged on an argument-by-argument basis. For example, if an override block sets only the create_before_destroy argument then any ignore_changes argument in the original block will be preserved.

If an overriding resource block contains one or more provisioner blocks then any provisioner blocks in the original block are ignored.

If an overriding resource block contains a connection block then it completely overrides any connection block present in the original block.

* refactor

* Merge locals/required_providers if possible

If there is only a single locals/required_providers in the primary,
we will merge it rather than append it to maintain backwards compatibility.

* Do not sort override files in PartialContent

For optimal performance, sort override files at parse time
instead of sorting in PartialContent all the time.

* Add E2E test for overriding required providers
  • Loading branch information
wata727 authored Oct 14, 2024
1 parent be12825 commit 47067d0
Show file tree
Hide file tree
Showing 11 changed files with 1,568 additions and 50 deletions.
4 changes: 4 additions & 0 deletions integrationtest/inspection/override/.tflint.hcl
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
plugin "testing" {
enabled = true
}

rule "terraform_required_providers" {
enabled = true
}
20 changes: 20 additions & 0 deletions integrationtest/inspection/override/result.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,25 @@
{
"issues": [
{
"rule": {
"name": "terraform_required_providers",
"severity": "error",
"link": ""
},
"message": "required_providers: aws=2,azurerm=1,google=3,oracle=2",
"range": {
"filename": "template.tf",
"start": {
"line": 11,
"column": 3
},
"end": {
"line": 11,
"column": 21
}
},
"callers": []
},
{
"rule": {
"name": "aws_instance_example_type",
Expand Down
12 changes: 12 additions & 0 deletions integrationtest/inspection/override/template.tf
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,15 @@ resource "aws_instance" "web" {
ami = "ami-12345678"
instance_type = "t2.micro" // Override by `template_override.tf`
}

terraform {
backend "s3" {}
}

terraform {
required_providers {
aws = "1" // Override by `template_override.tf`
google = "1" // Override by `version_override.tf`
azurerm = "1"
}
}
8 changes: 8 additions & 0 deletions integrationtest/inspection/override/template_override.tf
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,11 @@ resource "aws_instance" "web" {
instance_type = "m5.2xlarge" // aws_instance_example_type
iam_instance_profile = "web-server"
}

terraform {
required_providers {
aws = "2"
google = "2"
oracle = "2"
}
}
5 changes: 5 additions & 0 deletions integrationtest/inspection/override/version_override.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
terraform {
required_providers {
google = "3"
}
}
1 change: 1 addition & 0 deletions plugin/stub-generator/sources/testing/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ func main() {
rules.NewTerraformAutofixRemoveLocalRule(), // should be former than terraform_autofix_comment because this rule changes the line number
rules.NewTerraformAutofixCommentRule(),
rules.NewAwsInstanceAutofixConflictRule(), // should be later than terraform_autofix_comment because this rule adds an issue for terraform_autofix_comment
rules.NewTerraformRequiredProvidersRule(),
},
},
})
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package rules

import (
"fmt"
"sort"
"strings"

"github.com/terraform-linters/tflint-plugin-sdk/hclext"
"github.com/terraform-linters/tflint-plugin-sdk/tflint"
)

// TerraformRequiredProviders checks whether ...
type TerraformRequiredProviders struct {
tflint.DefaultRule
}

// NewTerraformRequiredProvidersRule returns a new rule
func NewTerraformRequiredProvidersRule() *TerraformRequiredProviders {
return &TerraformRequiredProviders{}
}

// Name returns the rule name
func (r *TerraformRequiredProviders) Name() string {
return "terraform_required_providers"
}

// Enabled returns whether the rule is enabled by default
func (r *TerraformRequiredProviders) Enabled() bool {
return false
}

// Severity returns the rule severity
func (r *TerraformRequiredProviders) Severity() tflint.Severity {
return tflint.ERROR
}

// Link returns the rule reference link
func (r *TerraformRequiredProviders) Link() string {
return ""
}

// Check checks whether ...
func (r *TerraformRequiredProviders) Check(runner tflint.Runner) error {
module, err := runner.GetModuleContent(&hclext.BodySchema{
Blocks: []hclext.BlockSchema{
{
Type: "terraform",
Body: &hclext.BodySchema{
Blocks: []hclext.BlockSchema{
{
Type: "required_providers",
Body: &hclext.BodySchema{Mode: hclext.SchemaJustAttributesMode},
},
},
},
},
},
}, nil)
if err != nil {
return err
}

for _, terraform := range module.Blocks {
for _, requiredProvider := range terraform.Body.Blocks {
ret := []string{}
for name, attr := range requiredProvider.Body.Attributes {
v, diags := attr.Expr.Value(nil)
if diags.HasErrors() {
return diags
}
ret = append(ret, fmt.Sprintf("%s=%s", name, v.AsString()))
}
sort.Strings(ret)

err := runner.EmitIssue(
r,
fmt.Sprintf("required_providers: %s", strings.Join(ret, ",")),
requiredProvider.DefRange,
)
if err != nil {
return err
}
}
}

return nil
}
Loading

0 comments on commit 47067d0

Please sign in to comment.