From 49d5d60cc80c88bfe8df381af50ded781369936a Mon Sep 17 00:00:00 2001 From: Daniel Banck Date: Tue, 3 May 2022 12:41:02 +0200 Subject: [PATCH] Extend earlydecoder to decode module calls (#106) * Add modules to the earlydecoder schema This will allow decoding modules blocks and their source and version attributes * Decode module blocks as part of loadModuleFromFile * Expose moduleCalls as part of module.Meta This adds moduleCalls as a new field to Meta and extends the decoder to return the decoded module calls. --- earlydecoder/decoder.go | 6 + earlydecoder/decoder_test.go | 315 ++++++++++++++++++++++++++++++----- earlydecoder/load_module.go | 27 +++ earlydecoder/schema.go | 15 ++ module/meta.go | 1 + 5 files changed, 324 insertions(+), 40 deletions(-) diff --git a/earlydecoder/decoder.go b/earlydecoder/decoder.go index f2bcfd45..efe2d5d2 100644 --- a/earlydecoder/decoder.go +++ b/earlydecoder/decoder.go @@ -163,6 +163,11 @@ func LoadModule(path string, files map[string]*hcl.File) (*module.Meta, hcl.Diag outputs[key] = *output } + modulesCalls := make(map[string]module.ModuleCall) + for key, moduleCall := range mod.ModuleCalls { + modulesCalls[key] = *moduleCall + } + return &module.Meta{ Path: path, Backend: backend, @@ -172,5 +177,6 @@ func LoadModule(path string, files map[string]*hcl.File) (*module.Meta, hcl.Diag Variables: variables, Outputs: outputs, Filenames: filenames, + ModuleCalls: modulesCalls, }, diags } diff --git a/earlydecoder/decoder_test.go b/earlydecoder/decoder_test.go index 616e87a3..0d6b33b6 100644 --- a/earlydecoder/decoder_test.go +++ b/earlydecoder/decoder_test.go @@ -41,6 +41,7 @@ func TestLoadModule(t *testing.T) { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -58,6 +59,7 @@ terraform { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -95,9 +97,10 @@ provider "grafana" { tfaddr.NewLegacyProvider("google"): {}, tfaddr.NewLegacyProvider("grafana"): {}, }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -135,9 +138,10 @@ provider "grafana" { tfaddr.NewLegacyProvider("google"): version.MustConstraints(version.NewConstraint(">= 3.0.0")), tfaddr.NewLegacyProvider("grafana"): {}, }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -179,9 +183,10 @@ provider "grafana" { tfaddr.NewLegacyProvider("google"): version.MustConstraints(version.NewConstraint(">= 3.0.0")), tfaddr.NewLegacyProvider("grafana"): {}, }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -253,9 +258,10 @@ provider "grafana" { Type: "grafana", }: version.MustConstraints(version.NewConstraint("2.1.0")), }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -317,9 +323,10 @@ resource "google_storage_bucket" "bucket" { Type: "google", }: version.MustConstraints(version.NewConstraint("2.0.0")), }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -382,9 +389,10 @@ resource "google_storage_bucket" "bucket" { Type: "google", }: version.MustConstraints(version.NewConstraint("2.0.0")), }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -439,9 +447,10 @@ provider "aws" { Type: "google", }: version.MustConstraints(version.NewConstraint("2.0.0")), }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -502,9 +511,10 @@ provider "aws" { Type: "google", }: version.MustConstraints(version.NewConstraint("2.0.0")), }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -540,9 +550,10 @@ resource "google_something" "test" { Type: "google-beta", }: version.MustConstraints(version.NewConstraint("2.0.0")), }, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -567,6 +578,7 @@ variable "" { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -582,6 +594,7 @@ variable { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, hcl.Diagnostics{ &hcl.Diagnostic{ @@ -629,6 +642,7 @@ variable "one" "two" { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, hcl.Diagnostics{ &hcl.Diagnostic{ @@ -678,8 +692,9 @@ variable "name" { Type: cty.DynamicPseudoType, }, }, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -698,8 +713,9 @@ variable "name" { Type: cty.String, }, }, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -719,8 +735,9 @@ variable "name" { Description: "description", }, }, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -740,8 +757,9 @@ variable "name" { IsSensitive: true, }, }, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -764,8 +782,9 @@ variable "name" { IsSensitive: true, }, }, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -785,8 +804,9 @@ variable "name" { DefaultValue: cty.EmptyObjectVal, }, }, - Outputs: map[string]module.Output{}, - Filenames: []string{"test.tf"}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -804,7 +824,8 @@ output "name" { Outputs: map[string]module.Output{ "name": {Value: cty.NilVal}, }, - Filenames: []string{"test.tf"}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -831,6 +852,7 @@ terraform { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -853,6 +875,7 @@ terraform { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -873,6 +896,7 @@ terraform { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -895,6 +919,7 @@ terraform { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, }, nil, }, @@ -922,6 +947,216 @@ terraform { Variables: map[string]module.Variable{}, Outputs: map[string]module.Output{}, Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, + }, + nil, + }, + } + + runTestCases(testCases, t, path) +} + +func TestLoadModule_Modules(t *testing.T) { + path := t.TempDir() + + testCases := []testCase{ + { + "no name module", + ` +module "" { +}`, + &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, + }, + nil, + }, + { + "no name modules", + ` +module { +}`, + &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, + }, + hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Missing name for module", + Detail: "All module blocks must have 1 labels (name).", + Subject: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 8, + Byte: 8, + }, + End: hcl.Pos{ + Line: 2, + Column: 9, + Byte: 9, + }, + }, + Context: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 1, + Byte: 1, + }, + End: hcl.Pos{ + Line: 2, + Column: 9, + Byte: 9, + }, + }, + }, + }, + }, + { + "double label modules", + ` +module "one" "two" { +}`, + &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{}, + }, + hcl.Diagnostics{ + &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Extraneous label for module", + Detail: "Only 1 labels (name) are expected for module blocks.", + Subject: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 14, + Byte: 14, + }, + End: hcl.Pos{ + Line: 2, + Column: 19, + Byte: 19, + }, + }, + Context: &hcl.Range{ + Filename: "test.tf", + Start: hcl.Pos{ + Line: 2, + Column: 1, + Byte: 1, + }, + End: hcl.Pos{ + Line: 2, + Column: 21, + Byte: 21, + }, + }, + }, + }, + }, + { + "empty modules", + ` +module "name" { +}`, + &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{ + "name": { + LocalName: "name", + }, + }, + }, + nil, + }, + { + "modules with source", + ` +module "name" { + source = "registry.terraform.io/terraform-aws-modules/vpc/aws" +}`, + &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{ + "name": { + LocalName: "name", + SourceAddr: "registry.terraform.io/terraform-aws-modules/vpc/aws", + }, + }, + }, + nil, + }, + { + "modules with version", + ` +module "name" { + version = "> 3.0.0, < 4.0.0" +}`, + &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{ + "name": { + LocalName: "name", + Version: "> 3.0.0, < 4.0.0", + }, + }, + }, + nil, + }, + { + "modules with source and version", + ` +module "name" { + source = "terraform-aws-modules/vpc/aws" + version = "1.0.0" +}`, + &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.ModuleCall{ + "name": { + LocalName: "name", + SourceAddr: "terraform-aws-modules/vpc/aws", + Version: "1.0.0", + }, + }, }, nil, }, diff --git a/earlydecoder/load_module.go b/earlydecoder/load_module.go index 01b94c69..82331b71 100644 --- a/earlydecoder/load_module.go +++ b/earlydecoder/load_module.go @@ -23,6 +23,7 @@ type decodedModule struct { DataSources map[string]*dataSource Variables map[string]*module.Variable Outputs map[string]*module.Output + ModuleCalls map[string]*module.ModuleCall } func newDecodedModule() *decodedModule { @@ -35,6 +36,7 @@ func newDecodedModule() *decodedModule { DataSources: make(map[string]*dataSource), Variables: make(map[string]*module.Variable), Outputs: make(map[string]*module.Output), + ModuleCalls: make(map[string]*module.ModuleCall), } } @@ -275,6 +277,31 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { IsSensitive: isSensitive, Value: value, } + case "module": + content, _, contentDiags := block.Body.PartialContent(moduleSchema) + diags = append(diags, contentDiags...) + if len(block.Labels) != 1 || block.Labels[0] == "" { + continue + } + name := block.Labels[0] + source := "" + version := "" + + var valDiags hcl.Diagnostics + if attr, defined := content.Attributes["source"]; defined { + valDiags = gohcl.DecodeExpression(attr.Expr, nil, &source) + diags = append(diags, valDiags...) + } + if attr, defined := content.Attributes["version"]; defined { + valDiags = gohcl.DecodeExpression(attr.Expr, nil, &version) + diags = append(diags, valDiags...) + } + + mod.ModuleCalls[name] = &module.ModuleCall{ + LocalName: name, + SourceAddr: source, + Version: version, + } } } diff --git a/earlydecoder/schema.go b/earlydecoder/schema.go index 2960222a..620ac66f 100644 --- a/earlydecoder/schema.go +++ b/earlydecoder/schema.go @@ -29,6 +29,10 @@ var rootSchema = &hcl.BodySchema{ Type: "output", LabelNames: []string{"name"}, }, + { + Type: "module", + LabelNames: []string{"name"}, + }, }, } @@ -98,3 +102,14 @@ var outputSchema = &hcl.BodySchema{ }, }, } + +var moduleSchema = &hcl.BodySchema{ + Attributes: []hcl.AttributeSchema{ + { + Name: "source", + }, + { + Name: "version", + }, + }, +} diff --git a/module/meta.go b/module/meta.go index c7478341..0e399e0f 100644 --- a/module/meta.go +++ b/module/meta.go @@ -16,6 +16,7 @@ type Meta struct { CoreRequirements version.Constraints Variables map[string]Variable Outputs map[string]Output + ModuleCalls map[string]ModuleCall } type ProviderRequirements map[tfaddr.Provider]version.Constraints