diff --git a/integrationtest/inspection/override/.tflint.hcl b/integrationtest/inspection/override/.tflint.hcl index e19f589dd..396bc0672 100644 --- a/integrationtest/inspection/override/.tflint.hcl +++ b/integrationtest/inspection/override/.tflint.hcl @@ -1,3 +1,7 @@ plugin "testing" { enabled = true } + +rule "terraform_required_providers" { + enabled = true +} diff --git a/integrationtest/inspection/override/result.json b/integrationtest/inspection/override/result.json index dfb6f374b..c5f563ba4 100644 --- a/integrationtest/inspection/override/result.json +++ b/integrationtest/inspection/override/result.json @@ -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", diff --git a/integrationtest/inspection/override/template.tf b/integrationtest/inspection/override/template.tf index 09d245ee3..55920459b 100644 --- a/integrationtest/inspection/override/template.tf +++ b/integrationtest/inspection/override/template.tf @@ -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" + } +} diff --git a/integrationtest/inspection/override/template_override.tf b/integrationtest/inspection/override/template_override.tf index c14a53457..3c6dbefc6 100644 --- a/integrationtest/inspection/override/template_override.tf +++ b/integrationtest/inspection/override/template_override.tf @@ -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" + } +} diff --git a/integrationtest/inspection/override/version_override.tf b/integrationtest/inspection/override/version_override.tf new file mode 100644 index 000000000..eeb64a5fd --- /dev/null +++ b/integrationtest/inspection/override/version_override.tf @@ -0,0 +1,5 @@ +terraform { + required_providers { + google = "3" + } +} diff --git a/plugin/stub-generator/sources/testing/main.go b/plugin/stub-generator/sources/testing/main.go index bfd71dd60..8bcd5a609 100644 --- a/plugin/stub-generator/sources/testing/main.go +++ b/plugin/stub-generator/sources/testing/main.go @@ -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(), }, }, }) diff --git a/plugin/stub-generator/sources/testing/rules/terraform_required_providers.go b/plugin/stub-generator/sources/testing/rules/terraform_required_providers.go new file mode 100644 index 000000000..e3cbf4713 --- /dev/null +++ b/plugin/stub-generator/sources/testing/rules/terraform_required_providers.go @@ -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 +} diff --git a/terraform/module.go b/terraform/module.go index e9f90eac4..b6ff59f35 100644 --- a/terraform/module.go +++ b/terraform/module.go @@ -21,8 +21,9 @@ type Module struct { Sources map[string][]byte Files map[string]*hcl.File - primaries map[string]*hcl.File - overrides map[string]*hcl.File + primaries map[string]*hcl.File + overrides map[string]*hcl.File + overrideFilenames []string } func NewEmptyModule() *Module { @@ -37,8 +38,9 @@ func NewEmptyModule() *Module { Sources: map[string][]byte{}, Files: map[string]*hcl.File{}, - primaries: map[string]*hcl.File{}, - overrides: map[string]*hcl.File{}, + primaries: map[string]*hcl.File{}, + overrides: map[string]*hcl.File{}, + overrideFilenames: []string{}, } } @@ -138,8 +140,10 @@ func (m *Module) PartialContent(schema *hclext.BodySchema, ctx *Evaluator) (*hcl } content.Blocks = append(content.Blocks, c.Blocks...) } - for _, f := range m.overrides { - expanded, d := ctx.ExpandBlock(f.Body, schema) + + // Overrides are processed in order first by filename (in lexicographical order) + for _, filename := range m.overrideFilenames { + expanded, d := ctx.ExpandBlock(m.overrides[filename].Body, schema) diags = diags.Extend(d) c, d := hclext.PartialContent(expanded, schema) diags = diags.Extend(d) @@ -152,25 +156,310 @@ func (m *Module) PartialContent(schema *hclext.BodySchema, ctx *Evaluator) (*hcl return content, diags } -// overrideBlocks changes the attributes in the passed primary blocks by override blocks recursively. +// blockAddr returns an identifier of the given block (e.g. "resource.aws_instance.main"). +// This is used for overrides, which use the block type and labels as identifiers. +func blockAddr(b *hclext.Block) string { + if len(b.Labels) > 0 { + return fmt.Sprintf("%s.%s", b.Type, strings.Join(b.Labels, ".")) + } + return b.Type +} + +// overrideBlocks overrides the primary blocks passed with override blocks, +// following Terraform's merge behavior. +// https://developer.hashicorp.com/terraform/language/files/override#merging-behavior +// +// Note that this function returns the overwritten primary blocks +// but has side effects on the primary blocks and the overrides blocks. func overrideBlocks(primaries, overrides hclext.Blocks) hclext.Blocks { - dict := map[string]*hclext.Block{} + overridesByAddr := map[string]hclext.Blocks{} for _, primary := range primaries { - key := fmt.Sprintf("%s[%s]", primary.Type, strings.Join(primary.Labels, ",")) - dict[key] = primary + addr := blockAddr(primary) + overridesByAddr[addr] = append(overridesByAddr[addr], primary) } + // The block containing elements that cannot be overridden will be added as new primaries. + // e.g. Local values ​not present in the primaries. + // + // Intuitively, if there is always only one corresponding block, + // it is hard to imagine a case where it cannot be overwritten, + // but since "locals" and "terraform" blocks can be declared multiple times, + // please note that the block to be overwritten cannot be uniquely determined. + newPrimaries := hclext.Blocks{} + for _, override := range overrides { - key := fmt.Sprintf("%s[%s]", override.Type, strings.Join(override.Labels, ",")) - if primary, exists := dict[key]; exists { - for name, attr := range override.Body.Attributes { + addr := blockAddr(override) + + switch override.Type { + case "resource": + if primaries, exists := overridesByAddr[addr]; exists { + // Duplicate resource blocks are not allowed. + overrideResourceBlock(primaries[0], override) + } + + // The "data" block is the same as generic block except for "depends_on". + // The "depends_on" arguments should not be merged, but Terraform will throw an error about it, + // so we won't take that into consideration here. + + // The "variable" block is the same as generic block except for "type" and "default". + // Conversion of default values ​​is done during evaluation and is not considered here. + + // The "output" block is the same as generic block except for "depends_on". + + case "locals": + remain := overrideLocalBlocks(overridesByAddr[addr], override) + if remain != nil { + newPrimaries = append(newPrimaries, remain) + } + + case "terraform": + remain := overrideTerraformBlocks(overridesByAddr[addr], override) + if remain != nil { + newPrimaries = append(newPrimaries, remain) + } + + default: + if primaries, exists := overridesByAddr[addr]; exists { + // The general rule, duplicated blocks are not allowed. + overrideGenericBlock(primaries[0], override) + } + } + } + + return append(primaries, newPrimaries...) +} + +// overrideResourceBlock overrides "resource" block +// https://developer.hashicorp.com/terraform/language/files/override#merging-resource-and-data-blocks +// +// The "depends_on" arguments should not be merged, but Terraform will throw an error about it, +// so we won't take that into consideration here. +// +// This function modifies the given primary directly. +func overrideResourceBlock(primary, override *hclext.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 + } + + // Exit early if blocks are empty. + if len(primary.Body.Blocks) == 0 && len(override.Body.Blocks) == 0 { + return + } + overridesByType := override.Body.Blocks.ByType() + + // 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. + primary.Body.Blocks = filterBlocks(primary.Body.Blocks, func(p *hclext.Block) bool { + overrides, exists := overridesByType[p.Type] + if !exists { + return true + } + + if p.Type == "lifecycle" { + // Contents of any lifecycle nested block are merged on an argument-by-argument basis. + // Can't override nested blocks like precondition/postcondition. + for _, override := range overrides { + for name, attr := range override.Body.Attributes { + p.Body.Attributes[name] = attr + } + } + return true + } + + return false + }) + primary.Body.Blocks = append( + primary.Body.Blocks, + filterBlocks(override.Body.Blocks, func(b *hclext.Block) bool { return b.Type != "lifecycle" })..., + ) +} + +// overrideLocalBlocks overrides "local" blocks +// https://developer.hashicorp.com/terraform/language/files/override#merging-locals-blocks +// +// This function modifies the given primaries directly. +// If the given override contains elements that cannot be overridden, (e.g. new local values) +// it is returned to the caller with only those elements remaining. +// This operation modifies the given override directly. +func overrideLocalBlocks(primaries hclext.Blocks, override *hclext.Block) *hclext.Block { + // When there are multiple locals blocks, + // it is not obvious into which one the remaining local values ​​should be merged. + appendRemains := len(primaries) > 1 + + // Tracks locals ​​that were not used to override. + remains := hclext.Attributes{} + for name, attr := range override.Body.Attributes { + remains[name] = attr + } + + // Overrides are applied on a value-by-value basis, ignoring which locals block they are defined in. + for _, primary := range primaries { + for name, attr := range override.Body.Attributes { + // Track the remaining local values ​​only if you need to append to them, + // otherwise simply merge them. + if appendRemains { + if _, exists := primary.Body.Attributes[name]; exists { + primary.Body.Attributes[name] = attr + delete(remains, name) + } + } else { primary.Body.Attributes[name] = attr } - primary.Body.Blocks = overrideBlocks(primary.Body.Blocks, override.Body.Blocks) } } - return primaries + // Any remaining locals that aren't overridden will be added as a new block. + if appendRemains && len(remains) > 0 { + override.Body.Attributes = remains + return override + } + return nil +} + +// overrideTerraformBlocks overrides "terraform" blocks +// https://developer.hashicorp.com/terraform/language/files/override#merging-terraform-blocks +// +// This function modifies the given primaries directly. +// If the given override contains elements that cannot be overridden, (e.g. new required providers) +// it is returned to the caller with only those elements remaining. +// This operation modifies the given override directly. +func overrideTerraformBlocks(primaries hclext.Blocks, override *hclext.Block) *hclext.Block { + // When there are multiple required_providers blocks, + // it is not obvious into which one the remaining require providers ​​should be merged. + appendRemains := false + requiredProviderSeen := false + for _, primary := range primaries { + switch len(primary.Body.Blocks.ByType()["required_providers"]) { + case 0: + continue + + case 1: + // Found multiple terraform blocks with required_providers + if requiredProviderSeen { + appendRemains = true + break + } + requiredProviderSeen = true + + default: + // Found terraform block with multiple required_providers + appendRemains = true + break + } + } + + // Tracks required providers ​​that were not used to override. + remainRequiredProviders := override.Body.Blocks.ByType()["required_providers"] + + for _, primary := range primaries { + // 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 + } + + // Exit early if blocks are empty. + if len(primary.Body.Blocks) == 0 && len(override.Body.Blocks) == 0 { + continue + } + overridesByType := override.Body.Blocks.ByType() + + // 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. + primary.Body.Blocks = filterBlocks(primary.Body.Blocks, func(p *hclext.Block) bool { + switch p.Type { + case "required_providers": + // If the required_providers argument is set, its value is merged on an element-by-element basis + for _, override := range overridesByType[p.Type] { + for name, attr := range override.Body.Attributes { + // Track the remaining required providers ​​only if you need to append to them, + // otherwise simply merge them. + if appendRemains { + if _, exists := p.Body.Attributes[name]; exists { + p.Body.Attributes[name] = attr + for _, remain := range remainRequiredProviders { + delete(remain.Body.Attributes, name) + } + } + } else { + p.Body.Attributes[name] = attr + } + } + } + return true + + 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. + if _, exists := overridesByType["cloud"]; exists { + return false + } + if _, exists := overridesByType["backend"]; exists { + return false + } + return true + + default: + _, exists := overridesByType[p.Type] + return !exists + } + }) + primary.Body.Blocks = append( + primary.Body.Blocks, + filterBlocks(override.Body.Blocks, func(b *hclext.Block) bool { return b.Type != "required_providers" })..., + ) + } + + // Any remaining required providers that aren't overridden will be added as a new block. + if appendRemains { + remainRequiredProviders = filterBlocks(remainRequiredProviders, func(b *hclext.Block) bool { + return len(b.Body.Attributes) > 0 + }) + if len(remainRequiredProviders) > 0 { + override.Body.Blocks = remainRequiredProviders + return override + } + } + return nil +} + +// overrideGenericBlock overrides generic blocks. +// https://developer.hashicorp.com/terraform/language/files/override#merging-behavior +// +// Except for a few special blocks, most blocks are overridden by this rule. +// This function modifies the given primary directly. +func overrideGenericBlock(primary, override *hclext.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 + } + + // Exit early if blocks are empty. + if len(primary.Body.Blocks) == 0 && len(override.Body.Blocks) == 0 { + return + } + overridesByType := override.Body.Blocks.ByType() + + // 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. + primary.Body.Blocks = filterBlocks(primary.Body.Blocks, func(p *hclext.Block) bool { + _, exists := overridesByType[p.Type] + return !exists + }) + primary.Body.Blocks = append(primary.Body.Blocks, override.Body.Blocks...) +} + +func filterBlocks(in hclext.Blocks, fn func(*hclext.Block) bool) hclext.Blocks { + out := hclext.Blocks{} + for _, block := range in { + if fn(block) { + out = append(out, block) + } + } + return out } var moduleSchema = &hclext.BodySchema{ diff --git a/terraform/module_test.go b/terraform/module_test.go index 20f472e42..e098cb0bf 100644 --- a/terraform/module_test.go +++ b/terraform/module_test.go @@ -236,19 +236,19 @@ resource "aws_instance" "bar" { Type: "resource", Labels: []string{"aws_instance", "foo"}, Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main1.tf"}}}, + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main1.tf", Start: hcl.Pos{Line: 3}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main1.tf"}, + DefRange: hcl.Range{Filename: "main1.tf", Start: hcl.Pos{Line: 2}}, }, { Type: "resource", Labels: []string{"aws_instance", "bar"}, Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main2.tf"}}}, + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main2.tf", Start: hcl.Pos{Line: 3}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main2.tf"}, + DefRange: hcl.Range{Filename: "main2.tf", Start: hcl.Pos{Line: 2}}, }, }, }, @@ -281,10 +281,53 @@ resource "aws_instance" "foo" { Type: "resource", Labels: []string{"aws_instance", "foo"}, Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main_override.tf"}}}, + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main_override.tf", Start: hcl.Pos{Line: 3}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2}}, + }, + }, + }, + }, + { + name: "overrides by multiple files/blocks", + files: map[string]string{ + "main.tf": ` +resource "aws_instance" "foo" { + instance_type = "t2.micro" +}`, + "main1_override.tf": ` +resource "aws_instance" "foo" { + instance_type = "m5.2xlarge" +}`, + "main2_override.tf": ` +resource "aws_instance" "foo" { + instance_type = "m5.4xlarge" +} +resource "aws_instance" "foo" { + instance_type = "m5.8xlarge" +}`, + }, + schema: &hclext.BodySchema{ + Attributes: []hclext.AttributeSchema{{Name: "foo"}}, + Blocks: []hclext.BlockSchema{ + { + Type: "resource", + LabelNames: []string{"type", "name"}, + Body: &hclext.BodySchema{Attributes: []hclext.AttributeSchema{{Name: "instance_type"}}}, + }, + }, + }, + want: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"aws_instance", "foo"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main2_override.tf", Start: hcl.Pos{Line: 6}}}}, + Blocks: hclext.Blocks{}, + }, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2}}, }, }, }, @@ -312,12 +355,12 @@ locals { Type: "locals", Body: &hclext.BodyContent{ Attributes: hclext.Attributes{ - "foo": &hclext.Attribute{Name: "foo", Range: hcl.Range{Filename: "main.tf"}}, - "bar": &hclext.Attribute{Name: "bar", Range: hcl.Range{Filename: "main.tf"}}, + "foo": &hclext.Attribute{Name: "foo", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3}}}, + "bar": &hclext.Attribute{Name: "bar", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4}}}, }, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2}}, }, }, }, @@ -351,19 +394,19 @@ resource "aws_instance" "bar" { Type: "resource", Labels: []string{"aws_instance", "bar"}, Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf"}}}, + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 8}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 6}}, }, { Type: "resource", Labels: []string{"aws_instance", "bar"}, Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf"}}}, + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 8}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 6}}, }, }, }, @@ -397,19 +440,19 @@ module "bar" { Type: "module", Labels: []string{"bar"}, Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf"}}}, + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 8}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 6}}, }, { Type: "module", Labels: []string{"bar"}, Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf"}}}, + Attributes: hclext.Attributes{"instance_type": &hclext.Attribute{Name: "instance_type", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 8}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 6}}, }, }, }, @@ -461,14 +504,14 @@ resource "aws_instance" "bar" { { Type: "ebs_block_device", Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf"}}}, + Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 4}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 3}}, }, }, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 2}}, }, { Type: "resource", @@ -479,22 +522,22 @@ resource "aws_instance" "bar" { { Type: "ebs_block_device", Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf"}}}, + Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 11}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 8}}, }, { Type: "ebs_block_device", Body: &hclext.BodyContent{ - Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf"}}}, + Attributes: hclext.Attributes{"volume_size": &hclext.Attribute{Name: "volume_size", Range: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 11}}}}, Blocks: hclext.Blocks{}, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 8}}, }, }, }, - DefRange: hcl.Range{Filename: "main.tf"}, + DefRange: hcl.Range{Filename: "main.tf", Start: hcl.Pos{Line: 7}}, }, }, }, @@ -539,7 +582,8 @@ resource "aws_instance" "bar" { opts := cmp.Options{ cmpopts.IgnoreFields(hclext.Block{}, "TypeRange", "LabelRanges"), cmpopts.IgnoreFields(hclext.Attribute{}, "Expr", "NameRange"), - cmpopts.IgnoreFields(hcl.Range{}, "Start", "End"), + cmpopts.IgnoreFields(hcl.Range{}, "End"), + cmpopts.IgnoreFields(hcl.Pos{}, "Column", "Byte"), cmpopts.SortSlices(func(i, j *hclext.Block) bool { return i.DefRange.String() < j.DefRange.String() }), @@ -568,7 +612,8 @@ func Test_overrideBlocks(t *testing.T) { Name: "no override", Primaries: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, }, @@ -577,7 +622,38 @@ func Test_overrideBlocks(t *testing.T) { Overrides: hclext.Blocks{}, Want: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, + }, + }, + }, + }, + { + Name: "no override because resources are difference", + Primaries: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"baz", "qux"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo2"}}, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, }, @@ -588,7 +664,8 @@ func Test_overrideBlocks(t *testing.T) { Name: "override", Primaries: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{ "foo": &hclext.Attribute{Name: "foo"}, @@ -599,21 +676,25 @@ func Test_overrideBlocks(t *testing.T) { }, Overrides: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{ "foo": &hclext.Attribute{Name: "bar"}, + "baz": &hclext.Attribute{Name: "baz"}, }, }, }, }, Want: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{ "foo": &hclext.Attribute{Name: "bar"}, "bar": &hclext.Attribute{Name: "bar"}, + "baz": &hclext.Attribute{Name: "baz"}, }, }, }, @@ -623,7 +704,8 @@ func Test_overrideBlocks(t *testing.T) { Name: "override nested blocks", Primaries: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}}, Blocks: hclext.Blocks{ @@ -642,7 +724,8 @@ func Test_overrideBlocks(t *testing.T) { }, Overrides: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "bar"}}, Blocks: hclext.Blocks{ @@ -651,6 +734,7 @@ func Test_overrideBlocks(t *testing.T) { Body: &hclext.BodyContent{ Attributes: hclext.Attributes{ "baz": &hclext.Attribute{Name: "qux"}, + "bar": &hclext.Attribute{Name: "bar"}, }, }, }, @@ -660,7 +744,8 @@ func Test_overrideBlocks(t *testing.T) { }, Want: hclext.Blocks{ { - Type: "resource", + Type: "resource", + Labels: []string{"foo", "bar"}, Body: &hclext.BodyContent{ Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "bar"}}, Blocks: hclext.Blocks{ @@ -668,7 +753,232 @@ func Test_overrideBlocks(t *testing.T) { Type: "nested", Body: &hclext.BodyContent{ Attributes: hclext.Attributes{ + // The contents of nested configuration blocks are not merged. "baz": &hclext.Attribute{Name: "qux"}, + "bar": &hclext.Attribute{Name: "bar"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override multiple nested blocks", + Primaries: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "foo": &hclext.Attribute{Name: "foo"}, + }, + }, + }, + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "bar": &hclext.Attribute{Name: "bar"}, + }, + }, + }, + { + Type: "other_nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "qux": &hclext.Attribute{Name: "qux"}, + }, + }, + }, + }, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "baz": &hclext.Attribute{Name: "baz"}, + }, + }, + }, + }, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + // Any block types that do not appear in the override block remain from the original block. + { + Type: "other_nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "qux": &hclext.Attribute{Name: "qux"}, + }, + }, + }, + // override block replace all blocks of the same type in the original block. + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "baz": &hclext.Attribute{Name: "baz"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override lifecycle/provisioner/connection", + Primaries: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "lifecycle", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"create_before_destroy": &hclext.Attribute{Name: "create_before_destroy"}, "prevent_destroy": &hclext.Attribute{Name: "prevent_destroy"}}, + }, + }, + { + Type: "provisioner", + Labels: []string{"local-exec"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"command": &hclext.Attribute{Name: "command"}}, + }, + }, + { + Type: "provisioner", + Labels: []string{"file"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"content": &hclext.Attribute{Name: "content"}}, + }, + }, + { + Type: "connection", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"type": &hclext.Attribute{Name: "type"}}, + }, + }, + }, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + { + Type: "lifecycle", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"ignore_changes": &hclext.Attribute{Name: "ignore_changes"}, "create_before_destroy": &hclext.Attribute{Name: "create_before_destroy2"}}, + }, + }, + { + Type: "provisioner", + Labels: []string{"remote-exec"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"inline": &hclext.Attribute{Name: "inline"}}, + }, + }, + { + Type: "connection", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"user": &hclext.Attribute{Name: "user"}}, + }, + }, + }, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "resource", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Blocks: hclext.Blocks{ + // the contents of any lifecycle nested block are merged on an argument-by-argument basis. + { + Type: "lifecycle", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"create_before_destroy": &hclext.Attribute{Name: "create_before_destroy2"}, "prevent_destroy": &hclext.Attribute{Name: "prevent_destroy"}, "ignore_changes": &hclext.Attribute{Name: "ignore_changes"}}, + }, + }, + // If an overriding resource block contains one or more provisioner blocks then any provisioner blocks in the original block are ignored. + { + Type: "provisioner", + Labels: []string{"remote-exec"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"inline": &hclext.Attribute{Name: "inline"}}, + }, + }, + // If an overriding resource block contains a connection block then it completely overrides any connection block present in the original block. + { + Type: "connection", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"user": &hclext.Attribute{Name: "user"}}, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override data sources", + Primaries: hclext.Blocks{ + { + Type: "data", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "foo": &hclext.Attribute{Name: "foo"}, + "bar": &hclext.Attribute{Name: "bar"}, + }, + Blocks: hclext.Blocks{ + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "foo": &hclext.Attribute{Name: "foo"}, + }, + }, + }, + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "bar": &hclext.Attribute{Name: "bar"}, + }, + }, + }, + { + Type: "other_nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ "qux": &hclext.Attribute{Name: "qux"}, }, }, @@ -677,6 +987,775 @@ func Test_overrideBlocks(t *testing.T) { }, }, }, + Overrides: hclext.Blocks{ + { + Type: "data", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "foo": &hclext.Attribute{Name: "bar"}, + "baz": &hclext.Attribute{Name: "baz"}, + }, + Blocks: hclext.Blocks{ + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "baz": &hclext.Attribute{Name: "baz"}, + }, + }, + }, + }, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "data", + Labels: []string{"foo", "bar"}, + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "foo": &hclext.Attribute{Name: "bar"}, + "bar": &hclext.Attribute{Name: "bar"}, + "baz": &hclext.Attribute{Name: "baz"}, + }, + Blocks: hclext.Blocks{ + { + Type: "other_nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "qux": &hclext.Attribute{Name: "qux"}, + }, + }, + }, + { + Type: "nested", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "baz": &hclext.Attribute{Name: "baz"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override locals", + Primaries: hclext.Blocks{ + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}, "bar": &hclext.Attribute{Name: "bar"}}, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar2"}, "foo2": &hclext.Attribute{Name: "foo2"}}, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{ + "foo": &hclext.Attribute{Name: "foo"}, + "bar": &hclext.Attribute{Name: "bar2"}, + "foo2": &hclext.Attribute{Name: "foo2"}, + }, + }, + }, + }, + }, + { + Name: "override multiple locals", + Primaries: hclext.Blocks{ + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}, "bar": &hclext.Attribute{Name: "bar"}}, + }, + }, + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"baz": &hclext.Attribute{Name: "baz"}, "qux": &hclext.Attribute{Name: "qux"}}, + }, + }, + }, + Overrides: hclext.Blocks{ + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"baz": &hclext.Attribute{Name: "baz2"}, "foo2": &hclext.Attribute{Name: "foo2"}}, + }, + }, + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"bar": &hclext.Attribute{Name: "bar2"}}, + }, + }, + }, + Want: hclext.Blocks{ + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"foo": &hclext.Attribute{Name: "foo"}, "bar": &hclext.Attribute{Name: "bar2"}}, + }, + }, + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"baz": &hclext.Attribute{Name: "baz2"}, "qux": &hclext.Attribute{Name: "qux"}}, + }, + }, + // Locals not present in the primaries are added. + { + Type: "locals", + Body: &hclext.BodyContent{ + Attributes: hclext.Attributes{"foo2": &hclext.Attribute{Name: "foo2"}}, + }, + }, + }, + }, + { + 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{ + // When overriding attributes, the last element in override takes precedence, + // so all attributes of primaries are overridden by required_version4. + { + 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: "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"}, + }, + }, + }, + }, + }, + }, + }, + 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{ + "google": &hclext.Attribute{Name: "google2"}, + "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"}, + }, + }, + }, + }, + }, + }, + }, + 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"}, + "azurerm": &hclext.Attribute{Name: "azurerm2"}, + "assert": &hclext.Attribute{Name: "assert"}, + "time": &hclext.Attribute{Name: "time"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override multiple 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{ + "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"}, + }, + }, + }, + }, + }, + }, + }, + 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: "google"}, + }, + }, + }, + { + 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{ + "azurerm": &hclext.Attribute{Name: "azurerm2"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override multiple terraform blocks with single 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: "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"}, + }, + }, + }, + }, + }, + }, + }, + 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: "google"}, + }, + }, + }, + }, + }, + }, + { + 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{ + "azurerm": &hclext.Attribute{Name: "azurerm2"}, + }, + }, + }, + }, + }, + }, + }, + }, + { + Name: "override multiple terraform blocks with multiple 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"}}, + }, + }, + }, + }, + }, + }, }, } diff --git a/terraform/parser.go b/terraform/parser.go index d5cd91cd7..32b999b87 100644 --- a/terraform/parser.go +++ b/terraform/parser.go @@ -65,6 +65,7 @@ func (p *Parser) LoadConfigDir(baseDir, dir string) (*Module, hcl.Diagnostics) { } mod := NewEmptyModule() + mod.overrideFilenames = make([]string, len(overrides)) for _, path := range primaries { f, loadDiags := p.loadHCLFile(baseDir, path) @@ -78,7 +79,7 @@ func (p *Parser) LoadConfigDir(baseDir, dir string) (*Module, hcl.Diagnostics) { mod.Sources[realPath] = f.Bytes mod.Files[realPath] = f } - for _, path := range overrides { + for idx, path := range overrides { f, loadDiags := p.loadHCLFile(baseDir, path) diags = diags.Extend(loadDiags) if loadDiags.HasErrors() { @@ -89,7 +90,10 @@ func (p *Parser) LoadConfigDir(baseDir, dir string) (*Module, hcl.Diagnostics) { mod.overrides[realPath] = f mod.Sources[realPath] = f.Bytes mod.Files[realPath] = f + mod.overrideFilenames[idx] = realPath } + // Overrides are processed in order first by filename (in lexicographical order) + sort.Strings(mod.overrideFilenames) if diags.HasErrors() { return mod, diags } diff --git a/terraform/parser_test.go b/terraform/parser_test.go index ecddd1f9c..45a731844 100644 --- a/terraform/parser_test.go +++ b/terraform/parser_test.go @@ -39,6 +39,7 @@ func TestLoadConfigDir(t *testing.T) { "main_override.tf": {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: "main_override.tf"}})}, "override.tf": {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: "override.tf"}})}, }, + overrideFilenames: []string{"main_override.tf", "override.tf"}, Sources: map[string][]byte{ "main.tf": {}, "main_override.tf": {}, @@ -69,6 +70,7 @@ func TestLoadConfigDir(t *testing.T) { "main_override.tf.json": {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: "main_override.tf.json"}})}, "override.tf.json": {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: "override.tf.json"}})}, }, + overrideFilenames: []string{"main_override.tf.json", "override.tf.json"}, Sources: map[string][]byte{ "main.tf.json": []byte("{}"), "main_override.tf.json": []byte("{}"), @@ -99,6 +101,7 @@ func TestLoadConfigDir(t *testing.T) { filepath.Join("foo", "main_override.tf"): {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: filepath.Join("foo", "main_override.tf")}})}, filepath.Join("foo", "override.tf"): {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: filepath.Join("foo", "override.tf")}})}, }, + overrideFilenames: []string{filepath.Join("foo", "main_override.tf"), filepath.Join("foo", "override.tf")}, Sources: map[string][]byte{ filepath.Join("foo", "main.tf"): {}, filepath.Join("foo", "main_override.tf"): {}, @@ -129,6 +132,7 @@ func TestLoadConfigDir(t *testing.T) { filepath.Join("bar", "main_override.tf"): {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: filepath.Join("bar", "main_override.tf")}})}, filepath.Join("bar", "override.tf"): {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: filepath.Join("bar", "override.tf")}})}, }, + overrideFilenames: []string{filepath.Join("bar", "main_override.tf"), filepath.Join("bar", "override.tf")}, Sources: map[string][]byte{ filepath.Join("bar", "main.tf"): {}, filepath.Join("bar", "main_override.tf"): {}, @@ -159,6 +163,7 @@ func TestLoadConfigDir(t *testing.T) { filepath.Join("foo", "bar", "main_override.tf"): {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: filepath.Join("foo", "bar", "main_override.tf")}})}, filepath.Join("foo", "bar", "override.tf"): {Body: hcltest.MockBody(&hcl.BodyContent{MissingItemRange: hcl.Range{Filename: filepath.Join("foo", "bar", "override.tf")}})}, }, + overrideFilenames: []string{filepath.Join("foo", "bar", "main_override.tf"), filepath.Join("foo", "bar", "override.tf")}, Sources: map[string][]byte{ filepath.Join("foo", "bar", "main.tf"): {}, filepath.Join("foo", "bar", "main_override.tf"): {}, @@ -216,6 +221,10 @@ func TestLoadConfigDir(t *testing.T) { t.Errorf("diff: %s", diff) } + if diff := cmp.Diff(mod.overrideFilenames, test.want.overrideFilenames); diff != "" { + t.Errorf("diff: %s", diff) + } + if diff := cmp.Diff(mod.Sources, test.want.Sources); diff != "" { t.Errorf("diff: %s", diff) }