diff --git a/config/interpolate.go b/config/interpolate.go new file mode 100644 index 000000000..7c0a9812c --- /dev/null +++ b/config/interpolate.go @@ -0,0 +1,157 @@ +package config + +import ( + "fmt" + "strconv" + "strings" +) + +// Interpolation is something that can be contained in a "${}" in a +// configuration value. +// +// Interpolations might be simple variable references, or it might be +// function calls, or even nested function calls. +type Interpolation interface { + FullString() string + Interpolate(map[string]string) (string, error) + Variables() map[string]InterpolatedVariable +} + +// An InterpolatedVariable is a variable reference within an interpolation. +// +// Implementations of this interface represents various sources where +// variables can come from: user variables, resources, etc. +type InterpolatedVariable interface { + FullKey() string +} + +// VariableInterpolation implements Interpolation for simple variable +// interpolation. Ex: "${var.foo}" or "${aws_instance.foo.bar}" +type VariableInterpolation struct { + Variable InterpolatedVariable + + key string +} + +func (i *VariableInterpolation) FullString() string { + return i.key +} + +func (i *VariableInterpolation) Interpolate( + vs map[string]string) (string, error) { + return vs[i.key], nil +} + +func (i *VariableInterpolation) Variables() map[string]InterpolatedVariable { + return map[string]InterpolatedVariable{i.key: i.Variable} +} + +// A ResourceVariable is a variable that is referencing the field +// of a resource, such as "${aws_instance.foo.ami}" +type ResourceVariable struct { + Type string // Resource type, i.e. "aws_instance" + Name string // Resource name + Field string // Resource field + + Multi bool // True if multi-variable: aws_instance.foo.*.id + Index int // Index for multi-variable: aws_instance.foo.1.id == 1 + + key string +} + +func NewResourceVariable(key string) (*ResourceVariable, error) { + parts := strings.SplitN(key, ".", 3) + field := parts[2] + multi := false + var index int + + if idx := strings.Index(field, "."); idx != -1 { + indexStr := field[:idx] + multi = indexStr == "*" + index = -1 + + if !multi { + indexInt, err := strconv.ParseInt(indexStr, 0, 0) + if err == nil { + multi = true + index = int(indexInt) + } + } + + if multi { + field = field[idx+1:] + } + } + + return &ResourceVariable{ + Type: parts[0], + Name: parts[1], + Field: field, + Multi: multi, + Index: index, + key: key, + }, nil +} + +func (v *ResourceVariable) ResourceId() string { + return fmt.Sprintf("%s.%s", v.Type, v.Name) +} + +func (v *ResourceVariable) FullKey() string { + return v.key +} + +// 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 NewUserVariable(key string) (*UserVariable, error) { + name := key[len("var."):] + return &UserVariable{ + key: key, + Name: name, + }, nil +} + +func (v *UserVariable) FullKey() string { + return v.key +} + +// A UserMapVariable is a variable that is referencing a user +// variable that is a map. This looks like "${var.amis.us-east-1}" +type UserMapVariable struct { + Name string + Elem string + + key string +} + +func NewUserMapVariable(key string) (*UserMapVariable, error) { + name := key[len("var."):] + idx := strings.Index(name, ".") + if idx == -1 { + return nil, fmt.Errorf("not a user map variable: %s", key) + } + + elem := name[idx+1:] + name = name[:idx] + return &UserMapVariable{ + Name: name, + Elem: elem, + + key: key, + }, nil +} + +func (v *UserMapVariable) FullKey() string { + return v.key +} + +func (v *UserMapVariable) GoString() string { + return fmt.Sprintf("%#v", *v) +} diff --git a/config/interpolate_test.go b/config/interpolate_test.go new file mode 100644 index 000000000..5f77f64b7 --- /dev/null +++ b/config/interpolate_test.go @@ -0,0 +1,136 @@ +package config + +import ( + "reflect" + "testing" +) + +func TestNewResourceVariable(t *testing.T) { + v, err := NewResourceVariable("foo.bar.baz") + if err != nil { + t.Fatalf("err: %s", err) + } + + if v.Type != "foo" { + t.Fatalf("bad: %#v", v) + } + if v.Name != "bar" { + t.Fatalf("bad: %#v", v) + } + if v.Field != "baz" { + t.Fatalf("bad: %#v", v) + } + if v.Multi { + t.Fatal("should not be multi") + } + + if v.FullKey() != "foo.bar.baz" { + t.Fatalf("bad: %#v", v) + } +} + +func TestNewUserVariable(t *testing.T) { + v, err := NewUserVariable("var.bar") + if err != nil { + t.Fatalf("err: %s", err) + } + + if v.Name != "bar" { + t.Fatalf("bad: %#v", v.Name) + } + if v.FullKey() != "var.bar" { + t.Fatalf("bad: %#v", v) + } +} + +func TestResourceVariable_impl(t *testing.T) { + var _ InterpolatedVariable = new(ResourceVariable) +} + +func TestResourceVariable_Multi(t *testing.T) { + v, err := NewResourceVariable("foo.bar.*.baz") + if err != nil { + t.Fatalf("err: %s", err) + } + + if v.Type != "foo" { + t.Fatalf("bad: %#v", v) + } + if v.Name != "bar" { + t.Fatalf("bad: %#v", v) + } + if v.Field != "baz" { + t.Fatalf("bad: %#v", v) + } + if !v.Multi { + t.Fatal("should be multi") + } +} + +func TestResourceVariable_MultiIndex(t *testing.T) { + cases := []struct { + Input string + Index int + Field string + }{ + {"foo.bar.*.baz", -1, "baz"}, + {"foo.bar.0.baz", 0, "baz"}, + {"foo.bar.5.baz", 5, "baz"}, + } + + for _, tc := range cases { + v, err := NewResourceVariable(tc.Input) + if err != nil { + t.Fatalf("err: %s", err) + } + if !v.Multi { + t.Fatalf("should be multi: %s", tc.Input) + } + if v.Index != tc.Index { + t.Fatalf("bad: %d\n\n%s", v.Index, tc.Input) + } + if v.Field != tc.Field { + t.Fatalf("bad: %s\n\n%s", v.Field, tc.Input) + } + } +} + +func TestUserVariable_impl(t *testing.T) { + var _ InterpolatedVariable = new(UserVariable) +} + +func TestUserMapVariable_impl(t *testing.T) { + var _ InterpolatedVariable = new(UserMapVariable) +} + +func TestVariableInterpolation_impl(t *testing.T) { + var _ Interpolation = new(VariableInterpolation) +} + +func TestVariableInterpolation(t *testing.T) { + uv, err := NewUserVariable("var.foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + i := &VariableInterpolation{Variable: uv, key: "var.foo"} + if i.FullString() != "var.foo" { + t.Fatalf("err: %#v", i) + } + + expected := map[string]InterpolatedVariable{"var.foo": uv} + if !reflect.DeepEqual(i.Variables(), expected) { + t.Fatalf("bad: %#v", i.Variables()) + } + + actual, err := i.Interpolate(map[string]string{ + "var.foo": "bar", + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual != "bar" { + t.Fatalf("bad: %#v", actual) + } +} diff --git a/config/variable.go b/config/variable.go index 96b49f9e6..d57463ae8 100644 --- a/config/variable.go +++ b/config/variable.go @@ -4,7 +4,6 @@ import ( "fmt" "reflect" "regexp" - "strconv" "strings" "github.com/mitchellh/reflectwalk" @@ -17,126 +16,6 @@ func init() { varRegexp = regexp.MustCompile(`(?i)(\$+)\{([*-.a-z0-9_]+)\}`) } -// 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 // Resource type, i.e. "aws_instance" - Name string // Resource name - Field string // Resource field - - Multi bool // True if multi-variable: aws_instance.foo.*.id - Index int // Index for multi-variable: aws_instance.foo.1.id == 1 - - key string -} - -func (v *ResourceVariable) ResourceId() string { - return fmt.Sprintf("%s.%s", v.Type, v.Name) -} - -func (v *ResourceVariable) FullKey() string { - return v.key -} - -// 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 (v *UserVariable) FullKey() string { - return v.key -} - -// A UserMapVariable is a variable that is referencing a user -// variable that is a map. This looks like "${var.amis.us-east-1}" -type UserMapVariable struct { - Name string - Elem string - - key string -} - -func NewUserMapVariable(key string) (*UserMapVariable, error) { - name := key[len("var."):] - idx := strings.Index(name, ".") - if idx == -1 { - return nil, fmt.Errorf("not a user map variable: %s", key) - } - - elem := name[idx+1:] - name = name[:idx] - return &UserMapVariable{ - Name: name, - Elem: elem, - - key: key, - }, nil -} - -func (v *UserMapVariable) FullKey() string { - return v.key -} - -func (v *UserMapVariable) GoString() string { - return fmt.Sprintf("%#v", *v) -} - -func NewResourceVariable(key string) (*ResourceVariable, error) { - parts := strings.SplitN(key, ".", 3) - field := parts[2] - multi := false - var index int - - if idx := strings.Index(field, "."); idx != -1 { - indexStr := field[:idx] - multi = indexStr == "*" - index = -1 - - if !multi { - indexInt, err := strconv.ParseInt(indexStr, 0, 0) - if err == nil { - multi = true - index = int(indexInt) - } - } - - if multi { - field = field[idx+1:] - } - } - - return &ResourceVariable{ - Type: parts[0], - Name: parts[1], - Field: field, - Multi: multi, - Index: index, - key: key, - }, nil -} - -func NewUserVariable(key string) (*UserVariable, error) { - name := key[len("var."):] - return &UserVariable{ - key: key, - Name: name, - }, nil -} - // ReplaceVariables takes a configuration and a mapping of variables // and performs the structure walking necessary to properly replace // all the variables. @@ -229,6 +108,7 @@ func (w *variableDetectWalker) Primitive(v reflect.Value) error { // will _panic_. The variableDetectWalker will tell you all variables // you need. type variableReplaceWalker struct { + Variables map[string]InterpolatedVariable Values map[string]string UnknownKeys []string diff --git a/config/variable_test.go b/config/variable_test.go index 1dacc4f41..803857880 100644 --- a/config/variable_test.go +++ b/config/variable_test.go @@ -36,44 +36,6 @@ func BenchmarkVariableReplaceWalker(b *testing.B) { } } -func TestNewResourceVariable(t *testing.T) { - v, err := NewResourceVariable("foo.bar.baz") - if err != nil { - t.Fatalf("err: %s", err) - } - - if v.Type != "foo" { - t.Fatalf("bad: %#v", v) - } - if v.Name != "bar" { - t.Fatalf("bad: %#v", v) - } - if v.Field != "baz" { - t.Fatalf("bad: %#v", v) - } - if v.Multi { - t.Fatal("should not be multi") - } - - if v.FullKey() != "foo.bar.baz" { - t.Fatalf("bad: %#v", v) - } -} - -func TestNewUserVariable(t *testing.T) { - v, err := NewUserVariable("var.bar") - if err != nil { - t.Fatalf("err: %s", err) - } - - if v.Name != "bar" { - t.Fatalf("bad: %#v", v.Name) - } - if v.FullKey() != "var.bar" { - t.Fatalf("bad: %#v", v) - } -} - func TestReplaceVariables(t *testing.T) { input := "foo-${var.bar}" expected := "foo-bar" @@ -93,66 +55,6 @@ func TestReplaceVariables(t *testing.T) { } } -func TestResourceVariable_impl(t *testing.T) { - var _ InterpolatedVariable = new(ResourceVariable) -} - -func TestResourceVariable_Multi(t *testing.T) { - v, err := NewResourceVariable("foo.bar.*.baz") - if err != nil { - t.Fatalf("err: %s", err) - } - - if v.Type != "foo" { - t.Fatalf("bad: %#v", v) - } - if v.Name != "bar" { - t.Fatalf("bad: %#v", v) - } - if v.Field != "baz" { - t.Fatalf("bad: %#v", v) - } - if !v.Multi { - t.Fatal("should be multi") - } -} - -func TestResourceVariable_MultiIndex(t *testing.T) { - cases := []struct { - Input string - Index int - Field string - }{ - {"foo.bar.*.baz", -1, "baz"}, - {"foo.bar.0.baz", 0, "baz"}, - {"foo.bar.5.baz", 5, "baz"}, - } - - for _, tc := range cases { - v, err := NewResourceVariable(tc.Input) - if err != nil { - t.Fatalf("err: %s", err) - } - if !v.Multi { - t.Fatalf("should be multi: %s", tc.Input) - } - if v.Index != tc.Index { - t.Fatalf("bad: %d\n\n%s", v.Index, tc.Input) - } - if v.Field != tc.Field { - t.Fatalf("bad: %s\n\n%s", v.Field, tc.Input) - } - } -} - -func TestUserVariable_impl(t *testing.T) { - var _ InterpolatedVariable = new(UserVariable) -} - -func TestUserMapVariable_impl(t *testing.T) { - var _ InterpolatedVariable = new(UserMapVariable) -} - func TestVariableDetectWalker(t *testing.T) { w := new(variableDetectWalker)