diff --git a/config/interpolate.go b/config/interpolate.go index dc4f39fd16..ab2cd26a40 100644 --- a/config/interpolate.go +++ b/config/interpolate.go @@ -2,10 +2,15 @@ package config import ( "fmt" + "regexp" "strconv" "strings" ) +// We really need to replace this with a real parser. +var funcRegexp *regexp.Regexp = regexp.MustCompile( + `(?i)([a-z0-9_]+)\(\s*(?:([.a-z0-9_]+)\s*,\s*)*([.a-z0-9_]+)\s*\)`) + // Interpolation is something that can be contained in a "${}" in a // configuration value. // @@ -17,6 +22,10 @@ type Interpolation interface { Variables() map[string]InterpolatedVariable } +// InterpolationFunc is the function signature for implementing +// callable functions in Terraform configurations. +type InterpolationFunc func(map[string]string, ...string) (string, error) + // An InterpolatedVariable is a variable reference within an interpolation. // // Implementations of this interface represents various sources where @@ -25,6 +34,15 @@ type InterpolatedVariable interface { FullKey() string } +// FunctionInterpolation is an Interpolation that executes a function +// with some variable number of arguments to generate a value. +type FunctionInterpolation struct { + Func InterpolationFunc + Args []InterpolatedVariable + + key string +} + // VariableInterpolation implements Interpolation for simple variable // interpolation. Ex: "${var.foo}" or "${aws_instance.foo.bar}" type VariableInterpolation struct { @@ -69,6 +87,33 @@ type UserMapVariable struct { // interpolation could not be found or the interpolation itself // is invalid. func NewInterpolation(v string) (Interpolation, error) { + match := funcRegexp.FindStringSubmatch(v) + if match != nil { + fn, ok := Funcs[match[1]] + if !ok { + return nil, fmt.Errorf( + "%s: Unknown function '%s'", + v, match[1]) + } + + args := make([]InterpolatedVariable, 0, len(match)-2) + for i := 2; i < len(match); i++ { + v, err := NewInterpolatedVariable(match[i]) + if err != nil { + return nil, err + } + + args = append(args, v) + } + + return &FunctionInterpolation{ + Func: fn, + Args: args, + + key: v, + }, nil + } + if idx := strings.Index(v, "."); idx >= 0 { v, err := NewInterpolatedVariable(v) if err != nil { @@ -99,6 +144,43 @@ func NewInterpolatedVariable(v string) (InterpolatedVariable, error) { } } +func (i *FunctionInterpolation) FullString() string { + return i.key +} + +func (i *FunctionInterpolation) Interpolate( + vs map[string]string) (string, error) { + args := make([]string, len(i.Args)) + for idx, a := range i.Args { + k := a.FullKey() + v, ok := vs[k] + if !ok { + return "", fmt.Errorf( + "%s: variable argument value unknown: %s", + i.FullString(), + k) + } + + args[idx] = v + } + + return i.Func(vs, args...) +} + +func (i *FunctionInterpolation) Variables() map[string]InterpolatedVariable { + result := make(map[string]InterpolatedVariable) + for _, a := range i.Args { + k := a.FullKey() + if _, ok := result[k]; ok { + continue + } + + result[k] = a + } + + return result +} + func (i *VariableInterpolation) FullString() string { return i.key } @@ -166,6 +248,10 @@ func (v *UserVariable) FullKey() string { return v.key } +func (v *UserVariable) GoString() string { + return fmt.Sprintf("*%#v", *v) +} + func NewUserMapVariable(key string) (*UserMapVariable, error) { name := key[len("var."):] idx := strings.Index(name, ".") diff --git a/config/interpolate_funcs.go b/config/interpolate_funcs.go new file mode 100644 index 0000000000..6e5ec98dfb --- /dev/null +++ b/config/interpolate_funcs.go @@ -0,0 +1,17 @@ +package config + +// Funcs is the mapping of built-in functions for configuration. +var Funcs map[string]InterpolationFunc + +func init() { + Funcs = map[string]InterpolationFunc{ + "lookup": interpolationFuncLookup, + } +} + +// interpolationFuncLookup implements the "lookup" function that allows +// dynamic lookups of map types within a Terraform configuration. +func interpolationFuncLookup( + vs map[string]string, args ...string) (string, error) { + return "", nil +} diff --git a/config/interpolate_test.go b/config/interpolate_test.go index 3e399bf655..87c2be0ca6 100644 --- a/config/interpolate_test.go +++ b/config/interpolate_test.go @@ -2,6 +2,7 @@ package config import ( "reflect" + "strings" "testing" ) @@ -28,6 +29,25 @@ func TestNewInterpolation(t *testing.T) { }, false, }, + + { + "lookup(var.foo, var.bar)", + &FunctionInterpolation{ + Func: nil, // Funcs["lookup"] + Args: []InterpolatedVariable{ + &UserVariable{ + Name: "foo", + key: "var.foo", + }, + &UserVariable{ + Name: "bar", + key: "var.bar", + }, + }, + key: "lookup(var.foo, var.bar)", + }, + false, + }, } for i, tc := range cases { @@ -35,6 +55,13 @@ func TestNewInterpolation(t *testing.T) { if (err != nil) != tc.Error { t.Fatalf("%d. Error: %s", i, err) } + + // This is jank, but reflect.DeepEqual never has functions + // being the same. + if f, ok := actual.(*FunctionInterpolation); ok { + f.Func = nil + } + if !reflect.DeepEqual(actual, tc.Result) { t.Fatalf("%d bad: %#v", i, actual) } @@ -106,6 +133,55 @@ func TestNewUserVariable(t *testing.T) { } } +func TestFunctionInterpolation_impl(t *testing.T) { + var _ Interpolation = new(FunctionInterpolation) +} + +func TestFunctionInterpolation(t *testing.T) { + v1, err := NewInterpolatedVariable("var.foo") + if err != nil { + t.Fatalf("err: %s", err) + } + + v2, err := NewInterpolatedVariable("var.bar") + if err != nil { + t.Fatalf("err: %s", err) + } + + fn := func(vs map[string]string, args ...string) (string, error) { + return strings.Join(args, " "), nil + } + + i := &FunctionInterpolation{ + Func: fn, + Args: []InterpolatedVariable{v1, v2}, + key: "foo", + } + if i.FullString() != "foo" { + t.Fatalf("err: %#v", i) + } + + expected := map[string]InterpolatedVariable{ + "var.foo": v1, + "var.bar": v2, + } + if !reflect.DeepEqual(i.Variables(), expected) { + t.Fatalf("bad: %#v", i.Variables()) + } + + actual, err := i.Interpolate(map[string]string{ + "var.foo": "bar", + "var.bar": "baz", + }) + if err != nil { + t.Fatalf("err: %s", err) + } + + if actual != "bar baz" { + t.Fatalf("bad: %#v", actual) + } +} + func TestResourceVariable_impl(t *testing.T) { var _ InterpolatedVariable = new(ResourceVariable) }