diff --git a/config/config.go b/config/config.go index 4932b3acb..b71ec6b6e 100644 --- a/config/config.go +++ b/config/config.go @@ -1,5 +1,9 @@ package config +import ( + "strings" +) + // Config is the configuration that comes from loading a collection // of Terraform templates. type Config struct { @@ -7,13 +11,71 @@ type Config struct { Resources []Resource } +// A resource represents a single Terraform resource in the configuration. +// A Terraform resource is something that represents some component that +// can be created and managed, and has some properties associated with it. type Resource struct { - Name string - Type string - Config map[string]interface{} + Name string + Type string + Config map[string]interface{} + Variables map[string]InterpolatedVariable } type Variable struct { Default string Description string } + +// An InterpolatedVariable is a variable that is embedded within a string +// in the configuration, such as "hello ${world}" (world in this case is +// an interpolated variable). +// +// These variables can come from a variety of sources, represented by +// implementations of this interface. +type InterpolatedVariable interface { + FullKey() string +} + +// A ResourceVariable is a variable that is referencing the field +// of a resource, such as "${aws_instance.foo.ami}" +type ResourceVariable struct { + Type string + Name string + Field string + + key string +} + +// A UserVariable is a variable that is referencing a user variable +// that is inputted from outside the configuration. This looks like +// "${var.foo}" +type UserVariable struct { + name string + key string +} + +func NewResourceVariable(key string) (*ResourceVariable, error) { + parts := strings.SplitN(key, ".", 3) + return &ResourceVariable{ + Type: parts[0], + Name: parts[1], + Field: parts[2], + key: key, + }, nil +} + +func (v *ResourceVariable) FullKey() string { + return v.key +} + +func NewUserVariable(key string) (*UserVariable, error) { + name := key[len("var."):] + return &UserVariable{ + key: key, + name: name, + }, nil +} + +func (v *UserVariable) FullKey() string { + return v.key +} diff --git a/config/loader_libucl.go b/config/loader_libucl.go index 41f118248..749f93dd6 100644 --- a/config/loader_libucl.go +++ b/config/loader_libucl.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/mitchellh/go-libucl" + "github.com/mitchellh/reflectwalk" ) // Put the parse flags we use for libucl in a constant so we can get @@ -176,10 +177,20 @@ func loadResourcesLibucl(o *libucl.Object) ([]Resource, error) { err) } + walker := new(variableDetectWalker) + if err := reflectwalk.Walk(config, walker); err != nil { + return nil, fmt.Errorf( + "Error reading config for %s[%s]: %s", + t.Key(), + r.Key(), + err) + } + result = append(result, Resource{ - Name: r.Key(), - Type: t.Key(), - Config: config, + Name: r.Key(), + Type: t.Key(), + Config: config, + Variables: walker.Variables, }) } } diff --git a/config/loader_test.go b/config/loader_test.go index 0a8fbb42f..e26687b66 100644 --- a/config/loader_test.go +++ b/config/loader_test.go @@ -69,6 +69,23 @@ func resourcesStr(rs []Resource) string { for k, _ := range r.Config { result += fmt.Sprintf(" %s\n", k) } + + if len(r.Variables) > 0 { + result += fmt.Sprintf(" vars\n") + for _, rawV := range r.Variables { + kind := "unknown" + str := rawV.FullKey() + + switch rawV.(type) { + case *ResourceVariable: + kind = "resource" + case *UserVariable: + kind = "user" + } + + result += fmt.Sprintf(" %s: %s\n", kind, str) + } + } } return strings.TrimSpace(result) @@ -101,6 +118,9 @@ aws_security_group[firewall] aws_instance[web] ami security_groups + vars + user: var.foo + resource: aws_security_group.firewall.foo ` const basicVariablesStr = ` diff --git a/config/test-fixtures/basic.tf b/config/test-fixtures/basic.tf index 846ee41f5..9eebc208a 100644 --- a/config/test-fixtures/basic.tf +++ b/config/test-fixtures/basic.tf @@ -7,7 +7,7 @@ resource "aws_security_group" "firewall" { } resource aws_instance "web" { - ami = "ami-123456" + ami = "${var.foo}" security_groups = [ "foo", "${aws_security_group.firewall.foo}" diff --git a/config/variable.go b/config/variable.go new file mode 100644 index 000000000..edf8b671c --- /dev/null +++ b/config/variable.go @@ -0,0 +1,70 @@ +package config + +import ( + "reflect" + "regexp" + "strings" +) + +// varRegexp is a regexp that matches variables such as ${foo.bar} +var varRegexp *regexp.Regexp + +func init() { + varRegexp = regexp.MustCompile(`(?i)(\$+)\{([-.a-z0-9_]+)\}`) +} + +// variableDetectWalker implements interfaces for the reflectwalk package +// (github.com/mitchellh/reflectwalk) that can be used to automatically +// pull out the variables that need replacing. +type variableDetectWalker struct { + Variables map[string]InterpolatedVariable +} + +func (w *variableDetectWalker) Primitive(v reflect.Value) error { + // We only care about strings + if v.Kind() != reflect.String { + return nil + } + + // XXX: This can be a lot more efficient if we used a real + // parser. A regexp is a hammer though that will get this working. + + matches := varRegexp.FindAllStringSubmatch(v.String(), -1) + if len(matches) == 0 { + return nil + } + + for _, match := range matches { + dollars := len(match[1]) + + // If there are even amounts of dollar signs, then it is escaped + if dollars%2 == 0 { + continue + } + + // Otherwise, record it + key := match[2] + if w.Variables == nil { + w.Variables = make(map[string]InterpolatedVariable) + } + if _, ok := w.Variables[key]; ok { + continue + } + + var err error + var iv InterpolatedVariable + if strings.HasPrefix(key, "var.") { + iv, err = NewUserVariable(key) + } else { + iv, err = NewResourceVariable(key) + } + + if err != nil { + return err + } + + w.Variables[key] = iv + } + + return nil +}