diff --git a/configs/module_call.go b/configs/module_call.go index 86f840216..94bd1181e 100644 --- a/configs/module_call.go +++ b/configs/module_call.go @@ -1,6 +1,8 @@ package configs import ( + "fmt" + "github.com/hashicorp/hcl2/gohcl" "github.com/hashicorp/hcl2/hcl" "github.com/hashicorp/hcl2/hcl/hclsyntax" @@ -21,6 +23,8 @@ type ModuleCall struct { Count hcl.Expression ForEach hcl.Expression + Providers []PassedProviderConfig + DependsOn []hcl.Traversal DeclRange hcl.Range @@ -76,9 +80,49 @@ func decodeModuleBlock(block *hcl.Block, override bool) (*ModuleCall, hcl.Diagno mc.DependsOn = append(mc.DependsOn, deps...) } + if attr, exists := content.Attributes["providers"]; exists { + seen := make(map[string]hcl.Range) + pairs, pDiags := hcl.ExprMap(attr.Expr) + diags = append(diags, pDiags...) + for _, pair := range pairs { + key, keyDiags := decodeProviderConfigRef(pair.Key, "providers") + diags = append(diags, keyDiags...) + value, valueDiags := decodeProviderConfigRef(pair.Value, "providers") + diags = append(diags, valueDiags...) + if keyDiags.HasErrors() || valueDiags.HasErrors() { + continue + } + + matchKey := key.String() + if prev, exists := seen[matchKey]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Duplicate provider address", + Detail: fmt.Sprintf("A provider configuration was already passed to %s at %s. Each child provider configuration can be assigned only once.", matchKey, prev), + Subject: pair.Value.Range().Ptr(), + }) + continue + } + + rng := hcl.RangeBetween(pair.Key.Range(), pair.Value.Range()) + seen[matchKey] = rng + mc.Providers = append(mc.Providers, PassedProviderConfig{ + InChild: key, + InParent: value, + }) + } + } + return mc, diags } +// PassedProviderConfig represents a provider config explicitly passed down to +// a child module, possibly giving it a new local address in the process. +type PassedProviderConfig struct { + InChild *ProviderConfigRef + InParent *ProviderConfigRef +} + var moduleBlockSchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ { @@ -97,5 +141,8 @@ var moduleBlockSchema = &hcl.BodySchema{ { Name: "depends_on", }, + { + Name: "providers", + }, }, } diff --git a/configs/module_call_test.go b/configs/module_call_test.go new file mode 100644 index 000000000..d86ddcc06 --- /dev/null +++ b/configs/module_call_test.go @@ -0,0 +1,137 @@ +package configs + +import ( + "io/ioutil" + "testing" + + "github.com/go-test/deep" + "github.com/hashicorp/hcl2/hcl" +) + +func TestLoadModuleCall(t *testing.T) { + src, err := ioutil.ReadFile("test-fixtures/valid-files/module-calls.tf") + if err != nil { + t.Fatal(err) + } + + parser := testParser(map[string]string{ + "module-calls.tf": string(src), + }) + + file, diags := parser.LoadConfigFile("module-calls.tf") + if len(diags) != 0 { + t.Errorf("Wrong number of diagnostics %d; want 0", len(diags)) + for _, diag := range diags { + t.Logf("- %s", diag) + } + return + } + + gotModules := file.ModuleCalls + wantModules := []*ModuleCall{ + { + Name: "foo", + SourceAddr: "./foo", + SourceSet: true, + SourceAddrRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 3, Column: 12, Byte: 27}, + End: hcl.Pos{Line: 3, Column: 19, Byte: 34}, + }, + DeclRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 2, Column: 1, Byte: 1}, + End: hcl.Pos{Line: 2, Column: 13, Byte: 13}, + }, + }, + { + Name: "bar", + SourceAddr: "hashicorp/bar/aws", + SourceSet: true, + SourceAddrRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 8, Column: 12, Byte: 113}, + End: hcl.Pos{Line: 8, Column: 31, Byte: 132}, + }, + DeclRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 7, Column: 1, Byte: 87}, + End: hcl.Pos{Line: 7, Column: 13, Byte: 99}, + }, + }, + { + Name: "baz", + SourceAddr: "git::https://example.com/", + SourceSet: true, + SourceAddrRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 15, Column: 12, Byte: 193}, + End: hcl.Pos{Line: 15, Column: 39, Byte: 220}, + }, + DependsOn: []hcl.Traversal{ + { + hcl.TraverseRoot{ + Name: "module", + SrcRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 23, Column: 5, Byte: 295}, + End: hcl.Pos{Line: 23, Column: 11, Byte: 301}, + }, + }, + hcl.TraverseAttr{ + Name: "bar", + SrcRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 23, Column: 11, Byte: 301}, + End: hcl.Pos{Line: 23, Column: 15, Byte: 305}, + }, + }, + }, + }, + Providers: []PassedProviderConfig{ + { + InChild: &ProviderConfigRef{ + Name: "aws", + NameRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 27, Column: 5, Byte: 332}, + End: hcl.Pos{Line: 27, Column: 8, Byte: 335}, + }, + }, + InParent: &ProviderConfigRef{ + Name: "aws", + NameRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 27, Column: 11, Byte: 338}, + End: hcl.Pos{Line: 27, Column: 14, Byte: 341}, + }, + Alias: "foo", + AliasRange: &hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 27, Column: 14, Byte: 341}, + End: hcl.Pos{Line: 27, Column: 18, Byte: 345}, + }, + }, + }, + }, + DeclRange: hcl.Range{ + Filename: "module-calls.tf", + Start: hcl.Pos{Line: 14, Column: 1, Byte: 167}, + End: hcl.Pos{Line: 14, Column: 13, Byte: 179}, + }, + }, + } + + // We'll hide all of the bodies/exprs since we're treating them as opaque + // here anyway... the point of this test is to ensure we handle everything + // else properly. + for _, m := range gotModules { + m.Config = nil + m.Count = nil + m.ForEach = nil + } + + for _, problem := range deep.Equal(gotModules, wantModules) { + t.Error(problem) + } +} diff --git a/configs/resource.go b/configs/resource.go index c3193bf61..18eb774f8 100644 --- a/configs/resource.go +++ b/configs/resource.go @@ -114,7 +114,7 @@ func decodeResourceBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { if attr, exists := content.Attributes["provider"]; exists { var providerDiags hcl.Diagnostics - r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr) + r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") diags = append(diags, providerDiags...) } @@ -290,7 +290,7 @@ func decodeDataBlock(block *hcl.Block) (*Resource, hcl.Diagnostics) { if attr, exists := content.Attributes["provider"]; exists { var providerDiags hcl.Diagnostics - r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr) + r.ProviderConfigRef, providerDiags = decodeProviderConfigRef(attr.Expr, "provider") diags = append(diags, providerDiags...) } @@ -324,10 +324,11 @@ type ProviderConfigRef struct { AliasRange *hcl.Range // nil if alias not set } -func decodeProviderConfigRef(attr *hcl.Attribute) (*ProviderConfigRef, hcl.Diagnostics) { +func decodeProviderConfigRef(expr hcl.Expression, argName string) (*ProviderConfigRef, hcl.Diagnostics) { var diags hcl.Diagnostics - expr, shimDiags := shimTraversalInString(attr.Expr, false) + var shimDiags hcl.Diagnostics + expr, shimDiags = shimTraversalInString(expr, false) diags = append(diags, shimDiags...) traversal, travDiags := hcl.AbsTraversalForExpr(expr) @@ -345,7 +346,7 @@ func decodeProviderConfigRef(attr *hcl.Attribute) (*ProviderConfigRef, hcl.Diagn // showing that usage, so we'll sniff for that situation here and // produce a specialized error message for it to help users find // the new correct form. - if exprIsNativeQuotedString(attr.Expr) { + if exprIsNativeQuotedString(expr) { diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid provider configuration reference", @@ -358,7 +359,7 @@ func decodeProviderConfigRef(attr *hcl.Attribute) (*ProviderConfigRef, hcl.Diagn diags = append(diags, &hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "Invalid provider configuration reference", - Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", attr.Name), + Detail: fmt.Sprintf("The %s argument requires a provider type name, optionally followed by a period and then a configuration alias.", argName), Subject: expr.Range().Ptr(), }) return nil, diags diff --git a/configs/test-fixtures/valid-files/module-calls.tf b/configs/test-fixtures/valid-files/module-calls.tf index abd423586..dbf29feef 100644 --- a/configs/test-fixtures/valid-files/module-calls.tf +++ b/configs/test-fixtures/valid-files/module-calls.tf @@ -22,5 +22,8 @@ module "baz" { depends_on = [ module.bar, ] -} + providers = { + aws = aws.foo + } +}