From 740c25d4ea1db7576900f73f236cd27beb3f71f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Tue, 13 Jan 2015 10:27:57 -0800 Subject: [PATCH] config: convert to config/lang --- config/config.go | 44 ++++++++---- config/interpolate_walk.go | 111 ++++++++++++------------------ config/interpolate_walk_test.go | 118 ++++++-------------------------- config/raw_config.go | 35 ++++++++-- 4 files changed, 125 insertions(+), 183 deletions(-) diff --git a/config/config.go b/config/config.go index abe60db6ef..7163e28295 100644 --- a/config/config.go +++ b/config/config.go @@ -8,6 +8,8 @@ import ( "strconv" "strings" + "github.com/hashicorp/terraform/config/lang" + "github.com/hashicorp/terraform/config/lang/ast" "github.com/hashicorp/terraform/flatmap" "github.com/hashicorp/terraform/helper/multierror" "github.com/mitchellh/mapstructure" @@ -169,7 +171,7 @@ func (c *Config) Validate() error { } interp := false - fn := func(i Interpolation) (string, error) { + fn := func(ast.Node) (string, error) { interp = true return "", nil } @@ -353,9 +355,18 @@ func (c *Config) Validate() error { } } - // Interpolate with a fixed number to verify that its a number - r.RawCount.interpolate(func(Interpolation) (string, error) { - return "5", nil + // Interpolate with a fixed number to verify that its a number. + r.RawCount.interpolate(func(root ast.Node) (string, error) { + // Execute the node but transform the AST so that it returns + // a fixed value of "5" for all interpolations. + var engine lang.Engine + out, _, err := engine.Execute(lang.FixedValueTransform( + root, &ast.LiteralNode{Value: "5", Type: ast.TypeString})) + if err != nil { + return "", err + } + + return out.(string), nil }) _, err := strconv.ParseInt(r.RawCount.Value().(string), 0, 0) if err != nil { @@ -465,20 +476,29 @@ func (c *Config) rawConfigs() map[string]*RawConfig { func (c *Config) validateVarContextFn( source string, errs *[]error) interpolationWalkerContextFunc { - return func(loc reflectwalk.Location, i Interpolation) { - vi, ok := i.(*VariableInterpolation) - if !ok { + return func(loc reflectwalk.Location, node ast.Node) { + if loc == reflectwalk.SliceElem { return } - rv, ok := vi.Variable.(*ResourceVariable) - if !ok { + vars, err := DetectVariables(node) + if err != nil { + // Ignore it since this will be caught during parse. This + // actually probably should never happen by the time this + // is called, but its okay. return } - if rv.Multi && rv.Index == -1 && loc != reflectwalk.SliceElem { - *errs = append(*errs, fmt.Errorf( - "%s: multi-variable must be in a slice", source)) + for _, v := range vars { + rv, ok := v.(*ResourceVariable) + if !ok { + return + } + + if rv.Multi && rv.Index == -1 { + *errs = append(*errs, fmt.Errorf( + "%s: multi-variable must be in a slice", source)) + } } } } diff --git a/config/interpolate_walk.go b/config/interpolate_walk.go index 7802537dce..17329e5a8a 100644 --- a/config/interpolate_walk.go +++ b/config/interpolate_walk.go @@ -3,9 +3,10 @@ package config import ( "fmt" "reflect" - "regexp" "strings" + "github.com/hashicorp/terraform/config/lang" + "github.com/hashicorp/terraform/config/lang/ast" "github.com/mitchellh/reflectwalk" ) @@ -14,10 +15,6 @@ import ( // a value that a user is very unlikely to use (such as UUID). const InterpSplitDelim = `B780FFEC-B661-4EB8-9236-A01737AD98B6` -// interpRegexp is a regexp that matches interpolations such as ${foo.bar} -var interpRegexp *regexp.Regexp = regexp.MustCompile( - `(?i)(\$+)\{([\s*-.,\\/\(\):a-z0-9_"]+)\}`) - // interpolationWalker implements interfaces for the reflectwalk package // (github.com/mitchellh/reflectwalk) that can be used to automatically // execute a callback for an interpolation. @@ -50,7 +47,7 @@ type interpolationWalker struct { // // If Replace is set to false in interpolationWalker, then the replace // value can be anything as it will have no effect. -type interpolationWalkerFunc func(Interpolation) (string, error) +type interpolationWalkerFunc func(ast.Node) (string, error) // interpolationWalkerContextFunc is called by interpolationWalk if // ContextF is set. This receives both the interpolation and the location @@ -58,7 +55,7 @@ type interpolationWalkerFunc func(Interpolation) (string, error) // // This callback can be used to validate the location of the interpolation // within the configuration. -type interpolationWalkerContextFunc func(reflectwalk.Location, Interpolation) +type interpolationWalkerContextFunc func(reflectwalk.Location, ast.Node) func (w *interpolationWalker) Enter(loc reflectwalk.Location) error { w.loc = loc @@ -121,76 +118,54 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error { 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. + astRoot, err := lang.Parse(v.String()) + if err != nil { + return err + } - matches := interpRegexp.FindAllStringSubmatch(v.String(), -1) - if len(matches) == 0 { + // If the AST we got is just a literal string value, then we ignore it + if _, ok := astRoot.(*ast.LiteralNode); ok { return nil } - result := v.String() - for _, match := range matches { - dollars := len(match[1]) + if w.ContextF != nil { + w.ContextF(w.loc, astRoot) + } - // If there are even amounts of dollar signs, then it is escaped - if dollars%2 == 0 { - continue - } + if w.F == nil { + return nil + } - // Interpolation found, instantiate it - key := match[2] - - i, err := ExprParse(key) - if err != nil { - return err - } - - if w.ContextF != nil { - w.ContextF(w.loc, i) - } - - if w.F == nil { - continue - } - - replaceVal, err := w.F(i) - if err != nil { - return fmt.Errorf( - "%s: %s", - key, - err) - } - - if w.Replace { - // We need to determine if we need to remove this element - // if the result contains any "UnknownVariableValue" which is - // set if it is computed. This behavior is different if we're - // splitting (in a SliceElem) or not. - remove := false - if w.loc == reflectwalk.SliceElem { - parts := strings.Split(replaceVal, InterpSplitDelim) - for _, p := range parts { - if p == UnknownVariableValue { - remove = true - break - } - } - } else if replaceVal == UnknownVariableValue { - remove = true - } - if remove { - w.removeCurrent() - return nil - } - - // Replace in our interpolation and continue on. - result = strings.Replace(result, match[0], replaceVal, -1) - } + replaceVal, err := w.F(astRoot) + if err != nil { + return fmt.Errorf( + "%s in:\n\n%s", + err, v.String()) } if w.Replace { - resultVal := reflect.ValueOf(result) + // We need to determine if we need to remove this element + // if the result contains any "UnknownVariableValue" which is + // set if it is computed. This behavior is different if we're + // splitting (in a SliceElem) or not. + remove := false + if w.loc == reflectwalk.SliceElem { + parts := strings.Split(replaceVal, InterpSplitDelim) + for _, p := range parts { + if p == UnknownVariableValue { + remove = true + break + } + } + } else if replaceVal == UnknownVariableValue { + remove = true + } + if remove { + w.removeCurrent() + return nil + } + + resultVal := reflect.ValueOf(replaceVal) switch w.loc { case reflectwalk.MapKey: m := w.cs[len(w.cs)-1] diff --git a/config/interpolate_walk_test.go b/config/interpolate_walk_test.go index 6e89286940..9b2c34133f 100644 --- a/config/interpolate_walk_test.go +++ b/config/interpolate_walk_test.go @@ -1,16 +1,18 @@ package config import ( + "fmt" "reflect" "testing" + "github.com/hashicorp/terraform/config/lang/ast" "github.com/mitchellh/reflectwalk" ) func TestInterpolationWalker_detect(t *testing.T) { cases := []struct { Input interface{} - Result []Interpolation + Result []string }{ { Input: map[string]interface{}{ @@ -23,13 +25,8 @@ func TestInterpolationWalker_detect(t *testing.T) { Input: map[string]interface{}{ "foo": "${var.foo}", }, - Result: []Interpolation{ - &VariableInterpolation{ - Variable: &UserVariable{ - Name: "foo", - key: "var.foo", - }, - }, + Result: []string{ + "Variable(var.foo)", }, }, @@ -37,19 +34,8 @@ func TestInterpolationWalker_detect(t *testing.T) { Input: map[string]interface{}{ "foo": "${aws_instance.foo.*.num}", }, - Result: []Interpolation{ - &VariableInterpolation{ - Variable: &ResourceVariable{ - Type: "aws_instance", - Name: "foo", - Field: "num", - - Multi: true, - Index: -1, - - key: "aws_instance.foo.*.num", - }, - }, + Result: []string{ + "Variable(aws_instance.foo.*.num)", }, }, @@ -57,18 +43,8 @@ func TestInterpolationWalker_detect(t *testing.T) { Input: map[string]interface{}{ "foo": "${lookup(var.foo)}", }, - Result: []Interpolation{ - &FunctionInterpolation{ - Func: nil, - Args: []Interpolation{ - &VariableInterpolation{ - Variable: &UserVariable{ - Name: "foo", - key: "var.foo", - }, - }, - }, - }, + Result: []string{ + "Call(lookup, Variable(var.foo))", }, }, @@ -76,15 +52,8 @@ func TestInterpolationWalker_detect(t *testing.T) { Input: map[string]interface{}{ "foo": `${file("test.txt")}`, }, - Result: []Interpolation{ - &FunctionInterpolation{ - Func: nil, - Args: []Interpolation{ - &LiteralInterpolation{ - Literal: "test.txt", - }, - }, - }, + Result: []string{ + "Call(file, Literal(TypeString, test.txt))", }, }, @@ -92,15 +61,8 @@ func TestInterpolationWalker_detect(t *testing.T) { Input: map[string]interface{}{ "foo": `${file("foo/bar.txt")}`, }, - Result: []Interpolation{ - &FunctionInterpolation{ - Func: nil, - Args: []Interpolation{ - &LiteralInterpolation{ - Literal: "foo/bar.txt", - }, - }, - }, + Result: []string{ + "Call(file, Literal(TypeString, foo/bar.txt))", }, }, @@ -108,25 +70,8 @@ func TestInterpolationWalker_detect(t *testing.T) { Input: map[string]interface{}{ "foo": `${join(",", foo.bar.*.id)}`, }, - Result: []Interpolation{ - &FunctionInterpolation{ - Func: nil, - Args: []Interpolation{ - &LiteralInterpolation{ - Literal: ",", - }, - &VariableInterpolation{ - Variable: &ResourceVariable{ - Type: "foo", - Name: "bar", - Field: "id", - Multi: true, - Index: -1, - key: "foo.bar.*.id", - }, - }, - }, - }, + Result: []string{ + "Call(join, Literal(TypeString, ,), Variable(foo.bar.*.id))", }, }, @@ -134,27 +79,16 @@ func TestInterpolationWalker_detect(t *testing.T) { Input: map[string]interface{}{ "foo": `${concat("localhost", ":8080")}`, }, - Result: []Interpolation{ - &FunctionInterpolation{ - Func: nil, - Args: []Interpolation{ - &LiteralInterpolation{ - Literal: "localhost", - }, - &LiteralInterpolation{ - Literal: ":8080", - }, - }, - }, + Result: []string{ + "Call(concat, Literal(TypeString, localhost), Literal(TypeString, :8080))", }, }, } for i, tc := range cases { - var actual []Interpolation - - detectFn := func(i Interpolation) (string, error) { - actual = append(actual, i) + var actual []string + detectFn := func(root ast.Node) (string, error) { + actual = append(actual, fmt.Sprintf("%s", root)) return "", nil } @@ -163,14 +97,6 @@ func TestInterpolationWalker_detect(t *testing.T) { t.Fatalf("err: %s", err) } - for _, a := range actual { - // This is jank, but reflect.DeepEqual never has functions - // being the same. - if f, ok := a.(*FunctionInterpolation); ok { - f.Func = nil - } - } - if !reflect.DeepEqual(actual, tc.Result) { t.Fatalf("%d: bad:\n\n%#v", i, actual) } @@ -198,7 +124,7 @@ func TestInterpolationWalker_replace(t *testing.T) { "foo": "hello, ${var.foo}", }, Output: map[string]interface{}{ - "foo": "hello, bar", + "foo": "bar", }, Value: "bar", }, @@ -247,7 +173,7 @@ func TestInterpolationWalker_replace(t *testing.T) { } for i, tc := range cases { - fn := func(i Interpolation) (string, error) { + fn := func(ast.Node) (string, error) { return tc.Value, nil } diff --git a/config/raw_config.go b/config/raw_config.go index e1c6c4108e..11481b087f 100644 --- a/config/raw_config.go +++ b/config/raw_config.go @@ -4,6 +4,8 @@ import ( "bytes" "encoding/gob" + "github.com/hashicorp/terraform/config/lang" + "github.com/hashicorp/terraform/config/lang/ast" "github.com/mitchellh/copystructure" "github.com/mitchellh/reflectwalk" ) @@ -26,7 +28,7 @@ const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66" type RawConfig struct { Key string Raw map[string]interface{} - Interpolations []Interpolation + Interpolations []ast.Node Variables map[string]InterpolatedVariable config map[string]interface{} @@ -79,8 +81,23 @@ func (r *RawConfig) Config() map[string]interface{} { // // If a variable key is missing, this will panic. func (r *RawConfig) Interpolate(vs map[string]string) error { - return r.interpolate(func(i Interpolation) (string, error) { - return i.Interpolate(vs) + varMap := make(map[string]lang.Variable) + for k, v := range vs { + varMap[k] = lang.Variable{Value: v, Type: ast.TypeString} + } + engine := &lang.Engine{ + GlobalScope: &lang.Scope{ + VarMap: varMap, + }, + } + + return r.interpolate(func(root ast.Node) (string, error) { + out, _, err := engine.Execute(root) + if err != nil { + return "", err + } + + return out.(string), nil }) } @@ -89,15 +106,19 @@ func (r *RawConfig) init() error { r.Interpolations = nil r.Variables = nil - fn := func(i Interpolation) (string, error) { - r.Interpolations = append(r.Interpolations, i) + fn := func(node ast.Node) (string, error) { + r.Interpolations = append(r.Interpolations, node) + vars, err := DetectVariables(node) + if err != nil { + return "", err + } - for k, v := range i.Variables() { + for _, v := range vars { if r.Variables == nil { r.Variables = make(map[string]InterpolatedVariable) } - r.Variables[k] = v + r.Variables[v.FullKey()] = v } return "", nil