config: convert to config/lang

This commit is contained in:
Mitchell Hashimoto 2015-01-13 10:27:57 -08:00
parent e68fbceebc
commit 740c25d4ea
4 changed files with 125 additions and 183 deletions

View File

@ -8,6 +8,8 @@ import (
"strconv" "strconv"
"strings" "strings"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
"github.com/hashicorp/terraform/flatmap" "github.com/hashicorp/terraform/flatmap"
"github.com/hashicorp/terraform/helper/multierror" "github.com/hashicorp/terraform/helper/multierror"
"github.com/mitchellh/mapstructure" "github.com/mitchellh/mapstructure"
@ -169,7 +171,7 @@ func (c *Config) Validate() error {
} }
interp := false interp := false
fn := func(i Interpolation) (string, error) { fn := func(ast.Node) (string, error) {
interp = true interp = true
return "", nil return "", nil
} }
@ -353,9 +355,18 @@ func (c *Config) Validate() error {
} }
} }
// Interpolate with a fixed number to verify that its a number // Interpolate with a fixed number to verify that its a number.
r.RawCount.interpolate(func(Interpolation) (string, error) { r.RawCount.interpolate(func(root ast.Node) (string, error) {
return "5", nil // 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) _, err := strconv.ParseInt(r.RawCount.Value().(string), 0, 0)
if err != nil { if err != nil {
@ -465,20 +476,29 @@ func (c *Config) rawConfigs() map[string]*RawConfig {
func (c *Config) validateVarContextFn( func (c *Config) validateVarContextFn(
source string, errs *[]error) interpolationWalkerContextFunc { source string, errs *[]error) interpolationWalkerContextFunc {
return func(loc reflectwalk.Location, i Interpolation) { return func(loc reflectwalk.Location, node ast.Node) {
vi, ok := i.(*VariableInterpolation) if loc == reflectwalk.SliceElem {
if !ok {
return return
} }
rv, ok := vi.Variable.(*ResourceVariable) vars, err := DetectVariables(node)
if !ok { 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 return
} }
if rv.Multi && rv.Index == -1 && loc != reflectwalk.SliceElem { for _, v := range vars {
*errs = append(*errs, fmt.Errorf( rv, ok := v.(*ResourceVariable)
"%s: multi-variable must be in a slice", source)) if !ok {
return
}
if rv.Multi && rv.Index == -1 {
*errs = append(*errs, fmt.Errorf(
"%s: multi-variable must be in a slice", source))
}
} }
} }
} }

View File

@ -3,9 +3,10 @@ package config
import ( import (
"fmt" "fmt"
"reflect" "reflect"
"regexp"
"strings" "strings"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
"github.com/mitchellh/reflectwalk" "github.com/mitchellh/reflectwalk"
) )
@ -14,10 +15,6 @@ import (
// a value that a user is very unlikely to use (such as UUID). // a value that a user is very unlikely to use (such as UUID).
const InterpSplitDelim = `B780FFEC-B661-4EB8-9236-A01737AD98B6` 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 // interpolationWalker implements interfaces for the reflectwalk package
// (github.com/mitchellh/reflectwalk) that can be used to automatically // (github.com/mitchellh/reflectwalk) that can be used to automatically
// execute a callback for an interpolation. // execute a callback for an interpolation.
@ -50,7 +47,7 @@ type interpolationWalker struct {
// //
// If Replace is set to false in interpolationWalker, then the replace // If Replace is set to false in interpolationWalker, then the replace
// value can be anything as it will have no effect. // 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 // interpolationWalkerContextFunc is called by interpolationWalk if
// ContextF is set. This receives both the interpolation and the location // 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 // This callback can be used to validate the location of the interpolation
// within the configuration. // within the configuration.
type interpolationWalkerContextFunc func(reflectwalk.Location, Interpolation) type interpolationWalkerContextFunc func(reflectwalk.Location, ast.Node)
func (w *interpolationWalker) Enter(loc reflectwalk.Location) error { func (w *interpolationWalker) Enter(loc reflectwalk.Location) error {
w.loc = loc w.loc = loc
@ -121,76 +118,54 @@ func (w *interpolationWalker) Primitive(v reflect.Value) error {
return nil return nil
} }
// XXX: This can be a lot more efficient if we used a real astRoot, err := lang.Parse(v.String())
// parser. A regexp is a hammer though that will get this working. if err != nil {
return err
}
matches := interpRegexp.FindAllStringSubmatch(v.String(), -1) // If the AST we got is just a literal string value, then we ignore it
if len(matches) == 0 { if _, ok := astRoot.(*ast.LiteralNode); ok {
return nil return nil
} }
result := v.String() if w.ContextF != nil {
for _, match := range matches { w.ContextF(w.loc, astRoot)
dollars := len(match[1]) }
// If there are even amounts of dollar signs, then it is escaped if w.F == nil {
if dollars%2 == 0 { return nil
continue }
}
// Interpolation found, instantiate it replaceVal, err := w.F(astRoot)
key := match[2] if err != nil {
return fmt.Errorf(
i, err := ExprParse(key) "%s in:\n\n%s",
if err != nil { err, v.String())
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)
}
} }
if w.Replace { 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 { switch w.loc {
case reflectwalk.MapKey: case reflectwalk.MapKey:
m := w.cs[len(w.cs)-1] m := w.cs[len(w.cs)-1]

View File

@ -1,16 +1,18 @@
package config package config
import ( import (
"fmt"
"reflect" "reflect"
"testing" "testing"
"github.com/hashicorp/terraform/config/lang/ast"
"github.com/mitchellh/reflectwalk" "github.com/mitchellh/reflectwalk"
) )
func TestInterpolationWalker_detect(t *testing.T) { func TestInterpolationWalker_detect(t *testing.T) {
cases := []struct { cases := []struct {
Input interface{} Input interface{}
Result []Interpolation Result []string
}{ }{
{ {
Input: map[string]interface{}{ Input: map[string]interface{}{
@ -23,13 +25,8 @@ func TestInterpolationWalker_detect(t *testing.T) {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": "${var.foo}", "foo": "${var.foo}",
}, },
Result: []Interpolation{ Result: []string{
&VariableInterpolation{ "Variable(var.foo)",
Variable: &UserVariable{
Name: "foo",
key: "var.foo",
},
},
}, },
}, },
@ -37,19 +34,8 @@ func TestInterpolationWalker_detect(t *testing.T) {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": "${aws_instance.foo.*.num}", "foo": "${aws_instance.foo.*.num}",
}, },
Result: []Interpolation{ Result: []string{
&VariableInterpolation{ "Variable(aws_instance.foo.*.num)",
Variable: &ResourceVariable{
Type: "aws_instance",
Name: "foo",
Field: "num",
Multi: true,
Index: -1,
key: "aws_instance.foo.*.num",
},
},
}, },
}, },
@ -57,18 +43,8 @@ func TestInterpolationWalker_detect(t *testing.T) {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": "${lookup(var.foo)}", "foo": "${lookup(var.foo)}",
}, },
Result: []Interpolation{ Result: []string{
&FunctionInterpolation{ "Call(lookup, Variable(var.foo))",
Func: nil,
Args: []Interpolation{
&VariableInterpolation{
Variable: &UserVariable{
Name: "foo",
key: "var.foo",
},
},
},
},
}, },
}, },
@ -76,15 +52,8 @@ func TestInterpolationWalker_detect(t *testing.T) {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": `${file("test.txt")}`, "foo": `${file("test.txt")}`,
}, },
Result: []Interpolation{ Result: []string{
&FunctionInterpolation{ "Call(file, Literal(TypeString, test.txt))",
Func: nil,
Args: []Interpolation{
&LiteralInterpolation{
Literal: "test.txt",
},
},
},
}, },
}, },
@ -92,15 +61,8 @@ func TestInterpolationWalker_detect(t *testing.T) {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": `${file("foo/bar.txt")}`, "foo": `${file("foo/bar.txt")}`,
}, },
Result: []Interpolation{ Result: []string{
&FunctionInterpolation{ "Call(file, Literal(TypeString, foo/bar.txt))",
Func: nil,
Args: []Interpolation{
&LiteralInterpolation{
Literal: "foo/bar.txt",
},
},
},
}, },
}, },
@ -108,25 +70,8 @@ func TestInterpolationWalker_detect(t *testing.T) {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": `${join(",", foo.bar.*.id)}`, "foo": `${join(",", foo.bar.*.id)}`,
}, },
Result: []Interpolation{ Result: []string{
&FunctionInterpolation{ "Call(join, Literal(TypeString, ,), Variable(foo.bar.*.id))",
Func: nil,
Args: []Interpolation{
&LiteralInterpolation{
Literal: ",",
},
&VariableInterpolation{
Variable: &ResourceVariable{
Type: "foo",
Name: "bar",
Field: "id",
Multi: true,
Index: -1,
key: "foo.bar.*.id",
},
},
},
},
}, },
}, },
@ -134,27 +79,16 @@ func TestInterpolationWalker_detect(t *testing.T) {
Input: map[string]interface{}{ Input: map[string]interface{}{
"foo": `${concat("localhost", ":8080")}`, "foo": `${concat("localhost", ":8080")}`,
}, },
Result: []Interpolation{ Result: []string{
&FunctionInterpolation{ "Call(concat, Literal(TypeString, localhost), Literal(TypeString, :8080))",
Func: nil,
Args: []Interpolation{
&LiteralInterpolation{
Literal: "localhost",
},
&LiteralInterpolation{
Literal: ":8080",
},
},
},
}, },
}, },
} }
for i, tc := range cases { for i, tc := range cases {
var actual []Interpolation var actual []string
detectFn := func(root ast.Node) (string, error) {
detectFn := func(i Interpolation) (string, error) { actual = append(actual, fmt.Sprintf("%s", root))
actual = append(actual, i)
return "", nil return "", nil
} }
@ -163,14 +97,6 @@ func TestInterpolationWalker_detect(t *testing.T) {
t.Fatalf("err: %s", err) 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) { if !reflect.DeepEqual(actual, tc.Result) {
t.Fatalf("%d: bad:\n\n%#v", i, actual) t.Fatalf("%d: bad:\n\n%#v", i, actual)
} }
@ -198,7 +124,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
"foo": "hello, ${var.foo}", "foo": "hello, ${var.foo}",
}, },
Output: map[string]interface{}{ Output: map[string]interface{}{
"foo": "hello, bar", "foo": "bar",
}, },
Value: "bar", Value: "bar",
}, },
@ -247,7 +173,7 @@ func TestInterpolationWalker_replace(t *testing.T) {
} }
for i, tc := range cases { for i, tc := range cases {
fn := func(i Interpolation) (string, error) { fn := func(ast.Node) (string, error) {
return tc.Value, nil return tc.Value, nil
} }

View File

@ -4,6 +4,8 @@ import (
"bytes" "bytes"
"encoding/gob" "encoding/gob"
"github.com/hashicorp/terraform/config/lang"
"github.com/hashicorp/terraform/config/lang/ast"
"github.com/mitchellh/copystructure" "github.com/mitchellh/copystructure"
"github.com/mitchellh/reflectwalk" "github.com/mitchellh/reflectwalk"
) )
@ -26,7 +28,7 @@ const UnknownVariableValue = "74D93920-ED26-11E3-AC10-0800200C9A66"
type RawConfig struct { type RawConfig struct {
Key string Key string
Raw map[string]interface{} Raw map[string]interface{}
Interpolations []Interpolation Interpolations []ast.Node
Variables map[string]InterpolatedVariable Variables map[string]InterpolatedVariable
config map[string]interface{} config map[string]interface{}
@ -79,8 +81,23 @@ func (r *RawConfig) Config() map[string]interface{} {
// //
// If a variable key is missing, this will panic. // If a variable key is missing, this will panic.
func (r *RawConfig) Interpolate(vs map[string]string) error { func (r *RawConfig) Interpolate(vs map[string]string) error {
return r.interpolate(func(i Interpolation) (string, error) { varMap := make(map[string]lang.Variable)
return i.Interpolate(vs) 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.Interpolations = nil
r.Variables = nil r.Variables = nil
fn := func(i Interpolation) (string, error) { fn := func(node ast.Node) (string, error) {
r.Interpolations = append(r.Interpolations, i) 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 { if r.Variables == nil {
r.Variables = make(map[string]InterpolatedVariable) r.Variables = make(map[string]InterpolatedVariable)
} }
r.Variables[k] = v r.Variables[v.FullKey()] = v
} }
return "", nil return "", nil