config: introduce Interpolation, not hooked up completely yet

This commit is contained in:
Mitchell Hashimoto 2014-07-21 10:39:55 -07:00
parent b772f8078d
commit 582b0cf43e
4 changed files with 294 additions and 219 deletions

157
config/interpolate.go Normal file
View File

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

136
config/interpolate_test.go Normal file
View File

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

View File

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

View File

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