From c6598a3f866326464ee3ec9b1ee6f4502312044f Mon Sep 17 00:00:00 2001 From: Martin Atkins Date: Mon, 23 Apr 2018 16:58:01 -0700 Subject: [PATCH] addrs: ParseAbsProviderConfig function This is for parsing the type of provider configuration address we write into state in order to remember which provider configuration is responsible for each resource. --- addrs/module_instance.go | 147 +++++++++++++++++++++++++- addrs/provider_config.go | 74 +++++++++++++ addrs/provider_config_test.go | 191 ++++++++++++++++++++++++++++++++++ 3 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 addrs/provider_config_test.go diff --git a/addrs/module_instance.go b/addrs/module_instance.go index 55fc71d87..afa4fc47f 100644 --- a/addrs/module_instance.go +++ b/addrs/module_instance.go @@ -1,6 +1,16 @@ package addrs -import "bytes" +import ( + "bytes" + "fmt" + + "github.com/zclconf/go-cty/cty/gocty" + + "github.com/zclconf/go-cty/cty" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/terraform/tfdiags" +) // ModuleInstance is an address for a particular module instance within the // dynamic module tree. This is an extension of the static traversals @@ -12,6 +22,141 @@ import "bytes" // creation. type ModuleInstance []ModuleInstanceStep +func ParseModuleInstance(traversal hcl.Traversal) (ModuleInstance, tfdiags.Diagnostics) { + mi, remain, diags := parseModuleInstancePrefix(traversal) + if len(remain) != 0 { + if len(remain) == len(traversal) { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid module instance address", + Detail: "A module instance address must begin with \"module.\".", + Subject: remain.SourceRange().Ptr(), + }) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid module instance address", + Detail: "The module instance address is followed by additional invalid content.", + Subject: remain.SourceRange().Ptr(), + }) + } + } + return mi, diags +} + +func parseModuleInstancePrefix(traversal hcl.Traversal) (ModuleInstance, hcl.Traversal, tfdiags.Diagnostics) { + remain := traversal + var mi ModuleInstance + var diags tfdiags.Diagnostics + + for len(remain) > 0 { + var next string + switch tt := remain[0].(type) { + case hcl.TraverseRoot: + next = tt.Name + case hcl.TraverseAttr: + next = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Module address prefix must be followed by dot and then a name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break + } + + if next != "module" { + break + } + + kwRange := remain[0].SourceRange() + remain = remain[1:] + // If we have the prefix "module" then we should be followed by an + // module call name, as an attribute, and then optionally an index step + // giving the instance key. + if len(remain) == 0 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: &kwRange, + }) + break + } + + var moduleName string + switch tt := remain[0].(type) { + case hcl.TraverseAttr: + moduleName = tt.Name + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Prefix \"module.\" must be followed by a module name.", + Subject: remain[0].SourceRange().Ptr(), + }) + break + } + remain = remain[1:] + step := ModuleInstanceStep{ + Name: moduleName, + } + + if len(remain) > 0 { + if idx, ok := remain[0].(hcl.TraverseIndex); ok { + remain = remain[1:] + + switch idx.Key.Type() { + case cty.String: + step.InstanceKey = StringKey(idx.Key.AsString()) + case cty.Number: + var idxInt int + err := gocty.FromCtyValue(idx.Key, &idxInt) + if err == nil { + step.InstanceKey = IntKey(idxInt) + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: fmt.Sprintf("Invalid module index: %s.", err), + Subject: idx.SourceRange().Ptr(), + }) + } + default: + // Should never happen, because no other types are allowed in traversal indices. + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid address operator", + Detail: "Invalid module key: must be either a string or an integer.", + Subject: idx.SourceRange().Ptr(), + }) + } + } + } + + mi = append(mi, step) + } + + var retRemain hcl.Traversal + if len(remain) > 0 { + retRemain = make(hcl.Traversal, len(remain)) + copy(retRemain, remain) + // The first element here might be either a TraverseRoot or a + // TraverseAttr, depending on whether we had a module address on the + // front. To make life easier for callers, we'll normalize to always + // start with a TraverseRoot. + if tt, ok := retRemain[0].(hcl.TraverseAttr); ok { + retRemain[0] = hcl.TraverseRoot{ + Name: tt.Name, + SrcRange: tt.SrcRange, + } + } + } + + return mi, retRemain, diags +} + // ModuleInstanceStep is a single traversal step through the dynamic module // tree. It is used only as part of ModuleInstance. type ModuleInstanceStep struct { diff --git a/addrs/provider_config.go b/addrs/provider_config.go index 57d4db6e1..b048b5987 100644 --- a/addrs/provider_config.go +++ b/addrs/provider_config.go @@ -2,6 +2,10 @@ package addrs import ( "fmt" + + "github.com/hashicorp/terraform/tfdiags" + + "github.com/hashicorp/hcl2/hcl" ) // ProviderConfig is the address of a provider configuration. @@ -37,6 +41,76 @@ type AbsProviderConfig struct { ProviderConfig ProviderConfig } +// ParseAbsProviderConfig parses the given traversal as an absolute provider +// address. The following are examples of traversals that can be successfully +// parsed as absolute provider configuration addresses: +// +// provider.aws +// provider.aws.foo +// module.bar.provider.aws +// module.bar.module.baz.provider.aws.foo +// module.foo[1].provider.aws.foo +// +// This type of address is used, for example, to record the relationships +// between resources and provider configurations in the state structure. +// This type of address is not generally used in the UI, except in error +// messages that refer to provider configurations. +func ParseAbsProviderConfig(traversal hcl.Traversal) (AbsProviderConfig, tfdiags.Diagnostics) { + modInst, remain, diags := parseModuleInstancePrefix(traversal) + ret := AbsProviderConfig{ + Module: modInst, + } + if len(remain) < 2 || remain.RootName() != "provider" { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration address", + Detail: "Provider address must begin with \"provider.\", followed by a provider type name.", + Subject: remain.SourceRange().Ptr(), + }) + return ret, diags + } + if len(remain) > 3 { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration address", + Detail: "Extraneous operators after provider configuration alias.", + Subject: hcl.Traversal(remain[3:]).SourceRange().Ptr(), + }) + return ret, diags + } + + if tt, ok := remain[1].(hcl.TraverseAttr); ok { + ret.ProviderConfig.Type = tt.Name + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration address", + Detail: "The prefix \"provider.\" must be followed by a provider type name.", + Subject: remain[1].SourceRange().Ptr(), + }) + return ret, diags + } + + if len(remain) == 3 { + if tt, ok := remain[2].(hcl.TraverseAttr); ok { + ret.ProviderConfig.Alias = tt.Name + } else { + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Invalid provider configuration address", + Detail: "Provider type name must be followed by a configuration alias name.", + Subject: remain[2].SourceRange().Ptr(), + }) + return ret, diags + } + } + + return ret, diags +} + func (pc AbsProviderConfig) String() string { + if len(pc.Module) == 0 { + return pc.ProviderConfig.String() + } return fmt.Sprintf("%s.%s", pc.Module.String(), pc.ProviderConfig.String()) } diff --git a/addrs/provider_config_test.go b/addrs/provider_config_test.go new file mode 100644 index 000000000..bea03bcac --- /dev/null +++ b/addrs/provider_config_test.go @@ -0,0 +1,191 @@ +package addrs + +import ( + "testing" + + "github.com/go-test/deep" + + "github.com/hashicorp/hcl2/hcl" + "github.com/hashicorp/hcl2/hcl/hclsyntax" +) + +func TestParseAbsProviderConfig(t *testing.T) { + tests := []struct { + Input string + Want AbsProviderConfig + WantDiag string + }{ + { + `provider.aws`, + AbsProviderConfig{ + Module: RootModuleInstance, + ProviderConfig: ProviderConfig{ + Type: "aws", + }, + }, + ``, + }, + { + `provider.aws.foo`, + AbsProviderConfig{ + Module: RootModuleInstance, + ProviderConfig: ProviderConfig{ + Type: "aws", + Alias: "foo", + }, + }, + ``, + }, + { + `module.baz.provider.aws`, + AbsProviderConfig{ + Module: ModuleInstance{ + { + Name: "baz", + }, + }, + ProviderConfig: ProviderConfig{ + Type: "aws", + }, + }, + ``, + }, + { + `module.baz.provider.aws.foo`, + AbsProviderConfig{ + Module: ModuleInstance{ + { + Name: "baz", + }, + }, + ProviderConfig: ProviderConfig{ + Type: "aws", + Alias: "foo", + }, + }, + ``, + }, + { + `module.baz["foo"].provider.aws`, + AbsProviderConfig{ + Module: ModuleInstance{ + { + Name: "baz", + InstanceKey: StringKey("foo"), + }, + }, + ProviderConfig: ProviderConfig{ + Type: "aws", + }, + }, + ``, + }, + { + `module.baz[1].provider.aws`, + AbsProviderConfig{ + Module: ModuleInstance{ + { + Name: "baz", + InstanceKey: IntKey(1), + }, + }, + ProviderConfig: ProviderConfig{ + Type: "aws", + }, + }, + ``, + }, + { + `module.baz[1].module.bar.provider.aws`, + AbsProviderConfig{ + Module: ModuleInstance{ + { + Name: "baz", + InstanceKey: IntKey(1), + }, + { + Name: "bar", + }, + }, + ProviderConfig: ProviderConfig{ + Type: "aws", + }, + }, + ``, + }, + { + `aws`, + AbsProviderConfig{}, + `Provider address must begin with "provider.", followed by a provider type name.`, + }, + { + `aws.foo`, + AbsProviderConfig{}, + `Provider address must begin with "provider.", followed by a provider type name.`, + }, + { + `provider`, + AbsProviderConfig{}, + `Provider address must begin with "provider.", followed by a provider type name.`, + }, + { + `provider.aws.foo.bar`, + AbsProviderConfig{}, + `Extraneous operators after provider configuration alias.`, + }, + { + `provider["aws"]`, + AbsProviderConfig{}, + `The prefix "provider." must be followed by a provider type name.`, + }, + { + `provider.aws["foo"]`, + AbsProviderConfig{}, + `Provider type name must be followed by a configuration alias name.`, + }, + { + `module.foo`, + AbsProviderConfig{}, + `Provider address must begin with "provider.", followed by a provider type name.`, + }, + { + `module.foo["provider"]`, + AbsProviderConfig{}, + `Provider address must begin with "provider.", followed by a provider type name.`, + }, + } + + for _, test := range tests { + t.Run(test.Input, func(t *testing.T) { + traversal, parseDiags := hclsyntax.ParseTraversalAbs([]byte(test.Input), "", hcl.Pos{}) + if len(parseDiags) != 0 { + t.Errorf("unexpected diagnostics during parse") + for _, diag := range parseDiags { + t.Logf("- %s", diag) + } + return + } + + got, diags := ParseAbsProviderConfig(traversal) + + if test.WantDiag != "" { + if len(diags) != 1 { + t.Fatalf("got %d diagnostics; want 1", len(diags)) + } + gotDetail := diags[0].Description().Detail + if gotDetail != test.WantDiag { + t.Fatalf("wrong diagnostic detail\ngot: %s\nwant: %s", gotDetail, test.WantDiag) + } + return + } else { + if len(diags) != 0 { + t.Fatalf("got %d diagnostics; want 0", len(diags)) + } + } + + for _, problem := range deep.Equal(got, test.Want) { + t.Error(problem) + } + }) + } +}