config: can detect variables in config strings

This commit is contained in:
Mitchell Hashimoto 2014-05-23 21:58:06 -07:00
parent 7e06b45232
commit 95ef186bf8
5 changed files with 170 additions and 7 deletions

View File

@ -1,5 +1,9 @@
package config package config
import (
"strings"
)
// Config is the configuration that comes from loading a collection // Config is the configuration that comes from loading a collection
// of Terraform templates. // of Terraform templates.
type Config struct { type Config struct {
@ -7,13 +11,71 @@ type Config struct {
Resources []Resource 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 { type Resource struct {
Name string Name string
Type string Type string
Config map[string]interface{} Config map[string]interface{}
Variables map[string]InterpolatedVariable
} }
type Variable struct { type Variable struct {
Default string Default string
Description 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
}

View File

@ -5,6 +5,7 @@ import (
"path/filepath" "path/filepath"
"github.com/mitchellh/go-libucl" "github.com/mitchellh/go-libucl"
"github.com/mitchellh/reflectwalk"
) )
// Put the parse flags we use for libucl in a constant so we can get // 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) 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{ result = append(result, Resource{
Name: r.Key(), Name: r.Key(),
Type: t.Key(), Type: t.Key(),
Config: config, Config: config,
Variables: walker.Variables,
}) })
} }
} }

View File

@ -69,6 +69,23 @@ func resourcesStr(rs []Resource) string {
for k, _ := range r.Config { for k, _ := range r.Config {
result += fmt.Sprintf(" %s\n", k) 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) return strings.TrimSpace(result)
@ -101,6 +118,9 @@ aws_security_group[firewall]
aws_instance[web] aws_instance[web]
ami ami
security_groups security_groups
vars
user: var.foo
resource: aws_security_group.firewall.foo
` `
const basicVariablesStr = ` const basicVariablesStr = `

View File

@ -7,7 +7,7 @@ resource "aws_security_group" "firewall" {
} }
resource aws_instance "web" { resource aws_instance "web" {
ami = "ami-123456" ami = "${var.foo}"
security_groups = [ security_groups = [
"foo", "foo",
"${aws_security_group.firewall.foo}" "${aws_security_group.firewall.foo}"

70
config/variable.go Normal file
View File

@ -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
}