terraform-linters / tflint

A Pluggable Terraform Linter
Mozilla Public License 2.0
4.98k stars 357 forks source link

Override exactly according to the Terraform spec #2124

Closed wata727 closed 1 month ago

wata727 commented 2 months ago

Fixes https://github.com/terraform-linters/tflint/issues/2114

As explained in #2114, the current override behavior is incomplete. The override behavior up to v0.53 is as follows:

# main.tf
resource "aws_instance" "foo" {
  instance_type = "t2.micro" # => Should be c5.xlarge, but can be m5.xlarge

  # The entire block is overwritten, the volume_type should be discarded, but only the volume_size is overwritten
  ebs_block_device {
    volume_type = "gp3"
    volume_size = 20
  }
}

# main1_override.tf
resource "aws_instance" "foo" {
  instance_type = "m5.xlarge"

  ebs_block_device {
    volume_size = 50
  }
}

# main2_override.tf
resource "aws_instance" "foo" {
  instance_type = "c5.xlarge"
}
# main.tf
terraform {
  backend "s3" {}
}

terraform {
  # If the terraform block below is overridden then "google" will be merged,
  # but if the one above is overridden then it will be ignored.
  required_providers {
    aws = {}
  }
}

# main_override.tf
terraform {
  required_providers {
    google = {}
  }
}

This PR fixes the override behavior to follow the Terraform spec. The following changes will be made:

In most cases, this change should be considered a bug fix as it results in stricter Terraform override behavior, and should have little to no impact unless you are doing complex overrides.

Finally, the behavior of overriding for multiply declarable blocks (e.g. terraform, locals) requires some caution: the GetModuleContent API returns different formats depending on whether there is one or more blocks.

For example, if only one required_providers is declared, the override will apply for that block:

# main.tf
terraform {
  # Only one block with required_providers is returned, containing 3 attributes: "aws", "google", and "azurerm".
  required_providers {
    aws = {}
  }
}

# main_override.tf
terraform {
  required_providers {
    google = {}
  }
}

terraform {
  required_providers {
    azurerm= {}
  }
}

On the other hand, if multiple required_providers are declared, the new attributes will be returned as a new block:

# main.tf
terraform {
  required_providers {
    aws = {}
  }
}

terraform {
  required_providers {
    google = {} # => This will be overridden
  }
}

# main_override.tf
terraform {
  required_providers {
    google = {}
    azurerm = {} # => This is not merged with either one and is returned as a new block, meaning the caller receives 3 "terraform" blocks.
  }
}

This is because it is not obvious which block the new attribute should be merged into.