Ejemplo n.º 1
0
func TestMerge(t *testing.T) {
	YAML := func(s string) map[interface{}]interface{} {
		y, err := simpleyaml.NewYaml([]byte(s))
		So(err, ShouldBeNil)

		data, err := y.Map()
		So(err, ShouldBeNil)

		return data
	}

	valueIs := func(tree interface{}, path string, expect string) {
		c, err := ParseCursor(path)
		So(err, ShouldBeNil)

		v, err := c.ResolveString(tree)
		So(err, ShouldBeNil)

		So(v, ShouldEqual, expect)
	}
	notPresent := func(tree interface{}, path string) {
		c, err := ParseCursor(path)
		So(err, ShouldBeNil)

		_, err = c.ResolveString(tree)
		So(err, ShouldNotBeNil)
		So(err.Error(), ShouldContainSubstring, "could not be found")
	}

	Convey("Merge()", t, func() {
		Convey("leaves original object untouched when merging", func() {
			template := YAML(`props:
  toplevel: TEMPLATE VALUE
  sub:
    key: ANOTHER TEMPLATE VALUE
`)
			other := YAML(`props:
  toplevel: override
`)

			merged, err := Merge(template, other)
			So(err, ShouldBeNil)

			valueIs(template, "props.toplevel", "TEMPLATE VALUE")
			valueIs(template, "props.sub.key", "ANOTHER TEMPLATE VALUE")

			valueIs(other, "props.toplevel", "override")
			notPresent(other, "props.sub.key")

			valueIs(merged, "props.toplevel", "override")
			valueIs(merged, "props.sub.key", "ANOTHER TEMPLATE VALUE")
		})
	})
}
Ejemplo n.º 2
0
func parseYAML(data []byte) (map[interface{}]interface{}, error) {
	y, err := simpleyaml.NewYaml(data)
	if err != nil {
		return nil, err
	}

	doc, err := y.Map()
	if err != nil {
		return nil, fmt.Errorf("Root of YAML document is not a hash/map: %s\n", err.Error())
	}

	return doc, nil
}
Ejemplo n.º 3
0
func jsonifyData(data []byte) (string, error) {
	y, err := simpleyaml.NewYaml(data)
	if err != nil {
		return "", err
	}

	doc, err := y.Map()
	if err != nil {
		return "", ansi.Errorf("@R{Root of YAML document is not a hash/map}: %s\n", err.Error())
	}

	b, err := json.Marshal(deinterface(doc))
	if err != nil {
		return "", err
	}

	return string(b), nil
}
Ejemplo n.º 4
0
func TestOperators(t *testing.T) {
	cursor := func(s string) *tree.Cursor {
		c, err := tree.ParseCursor(s)
		So(err, ShouldBeNil)
		return c
	}

	YAML := func(s string) map[interface{}]interface{} {
		y, err := simpleyaml.NewYaml([]byte(s))
		So(err, ShouldBeNil)

		data, err := y.Map()
		So(err, ShouldBeNil)

		return data
	}

	ref := func(s string) *Expr {
		return &Expr{Type: Reference, Reference: cursor(s)}
	}
	str := func(s string) *Expr {
		return &Expr{Type: Literal, Literal: s}
	}
	num := func(v int64) *Expr {
		return &Expr{Type: Literal, Literal: v}
	}
	null := func() *Expr {
		return &Expr{Type: Literal, Literal: nil}
	}
	env := func(s string) *Expr {
		return &Expr{Type: EnvVar, Name: s}
	}
	or := func(l *Expr, r *Expr) *Expr {
		return &Expr{Type: LogicalOr, Left: l, Right: r}
	}

	var exprOk func(*Expr, *Expr)
	exprOk = func(got *Expr, want *Expr) {
		So(got, ShouldNotBeNil)
		So(want, ShouldNotBeNil)

		So(got.Type, ShouldEqual, want.Type)
		switch want.Type {
		case Literal:
			So(got.Literal, ShouldEqual, want.Literal)

		case Reference:
			So(got.Reference.String(), ShouldEqual, want.Reference.String())

		case LogicalOr:
			exprOk(got.Left, want.Left)
			exprOk(got.Right, want.Right)
		}
	}

	Convey("Parser", t, func() {
		Convey("parses op calls in their entirety", func() {
			phase := EvalPhase

			opOk := func(code string, name string, args ...*Expr) {
				op, err := ParseOpcall(phase, code)
				So(err, ShouldBeNil)
				So(op, ShouldNotBeNil)

				_, ok := op.op.(NullOperator)
				So(ok, ShouldBeTrue)
				So(op.op.(NullOperator).Missing, ShouldEqual, name)

				So(len(op.args), ShouldEqual, len(args))
				for i, expect := range args {
					exprOk(op.args[i], expect)
				}
			}

			opErr := func(code string, msg string) {
				_, err := ParseOpcall(phase, code)
				So(err, ShouldNotBeNil)
				So(err.Error(), ShouldContainSubstring, msg)
			}

			Convey("handles opcodes with and without arguments", func() {
				opOk(`(( null ))`, "null")
				opOk(`(( null 42 ))`, "null", num(42))
				opOk(`(( null 1 2 3 4 ))`, "null", num(1), num(2), num(3), num(4))
			})

			Convey("ignores optional whitespace", func() {
				opOk(`((null))`, "null")
				opOk(`((	null	))`, "null")
				opOk(`((  	null  	))`, "null")

				args := []*Expr{num(1), num(2), num(3)}
				opOk(`((null 1 2 3))`, "null", args...)
				opOk(`((null 1	2	3))`, "null", args...)
				opOk(`((null 1	2	3	))`, "null", args...)
				opOk(`((null 1 	 2 	 3 	 ))`, "null", args...)
			})

			Convey("allows use of commas to separate arguments", func() {
				args := []*Expr{num(1), num(2), num(3)}
				opOk(`((null 1, 2, 3))`, "null", args...)
				opOk(`((null 1,	2,	3))`, "null", args...)
				opOk(`((null 1,	2,	3,	))`, "null", args...)
				opOk(`((null 1 ,	 2 ,	 3 ,	 ))`, "null", args...)
			})

			Convey("allows use of parentheses around arguments", func() {
				args := []*Expr{num(1), num(2), num(3)}
				opOk(`((null(1,2,3)))`, "null", args...)
				opOk(`((null(1, 2, 3) ))`, "null", args...)
				opOk(`((null( 1,	2,	3)))`, "null", args...)
				opOk(`((null (1,	2,	3)	))`, "null", args...)
				opOk(`((null (1 ,	 2 ,	 3)	 ))`, "null", args...)
			})

			Convey("handles string literal arguments", func() {
				opOk(`(( null "string" ))`, "null", str("string"))
				opOk(`(( null "string with whitespace" ))`, "null", str("string with whitespace"))
				opOk(`(( null "a \"quoted\" string" ))`, "null", str(`a "quoted" string`))
				opOk(`(( null "\\escaped" ))`, "null", str(`\escaped`))
			})

			Convey("handles reference (cursor) arguments", func() {
				opOk(`(( null x.y.z ))`, "null", ref("x.y.z"))
				opOk(`(( null x.[0].z ))`, "null", ref("x.0.z"))
				opOk(`(( null x[0].z ))`, "null", ref("x.0.z"))
				opOk(`(( null x[0]z ))`, "null", ref("x.0.z"))
			})

			Convey("handles mixed collections of argument types", func() {
				opOk(`(( xyzzy "string" x.y.z 42  ))`, "xyzzy", str("string"), ref("x.y.z"), num(42))
				opOk(`(( xyzzy("string" x.y.z 42) ))`, "xyzzy", str("string"), ref("x.y.z"), num(42))
			})

			Convey("handles expression-based operands", func() {
				opOk(`(( null meta.key || "default" ))`, "null",
					or(ref("meta.key"), str("default")))

				opOk(`(( null meta.key || "default" "second" ))`, "null",
					or(ref("meta.key"), str("default")),
					str("second"))

				opOk(`(( null meta.key || "default", "second" ))`, "null",
					or(ref("meta.key"), str("default")),
					str("second"))

				opOk(`(( null meta.key || "default", meta.other || nil ))`, "null",
					or(ref("meta.key"), str("default")),
					or(ref("meta.other"), null()))

				opOk(`(( null meta.key || "default"     meta.other || nil ))`, "null",
					or(ref("meta.key"), str("default")),
					or(ref("meta.other"), null()))
			})

			Convey("handles environment variables as operands", func() {
				os.Setenv("SPRUCE_FOO", "first test")
				os.Setenv("_SPRUCE", "_sprucify!")
				os.Setenv("ENOENT", "")
				os.Setenv("http_proxy", "no://thank/you")

				opOk(`(( null $SPRUCE_FOO ))`, "null", env("SPRUCE"))
				opOk(`(( null $_SPRUCE ))`, "null", env("_SPRUCE"))
				opOk(`(( null $ENOENT || $SPRUCE_FOO ))`, "null",
					or(env("ENOENT"), env("SPRUCE_FOO")))
				opOk(`(( null $http_proxy))`, "null", env("http_proxy"))
			})

			Convey("throws errors for malformed expression", func() {
				opErr(`(( null meta.key ||, nil ))`,
					`syntax error near: meta.key ||, nil`)

				opErr(`(( null || ))`,
					`syntax error near: ||`)

				opErr(`(( null || meta.key ))`,
					`syntax error near: || meta.key`)

				opErr(`(( null meta.key || || ))`,
					`syntax error near: meta.key || ||`)
			})
		})
	})

	Convey("Expression Engine", t, func() {
		var e *Expr
		var tree map[interface{}]interface{}

		evaluate := func(e *Expr, tree map[interface{}]interface{}) interface{} {
			v, err := e.Evaluate(tree)
			So(err, ShouldBeNil)
			return v
		}

		Convey("Literals evaluate to themselves", func() {
			e = &Expr{Type: Literal, Literal: "value"}
			So(evaluate(e, tree), ShouldEqual, "value")

			e = &Expr{Type: Literal, Literal: ""}
			So(evaluate(e, tree), ShouldEqual, "")

			e = &Expr{Type: Literal, Literal: nil}
			So(evaluate(e, tree), ShouldEqual, nil)
		})

		Convey("References evaluate to the referenced part of the YAML tree", func() {
			tree = YAML(`---
meta:
  foo: FOO
  bar: BAR
`)

			e = &Expr{Type: Reference, Reference: cursor("meta.foo")}
			So(evaluate(e, tree), ShouldEqual, "FOO")

			e = &Expr{Type: Reference, Reference: cursor("meta.bar")}
			So(evaluate(e, tree), ShouldEqual, "BAR")
		})

		Convey("|| operator evaluates to the first found value", func() {
			tree = YAML(`---
meta:
  foo: FOO
  bar: BAR
`)

			So(evaluate(or(str("first"), str("second")), tree), ShouldEqual, "first")
			So(evaluate(or(ref("meta.foo"), str("second")), tree), ShouldEqual, "FOO")
			So(evaluate(or(ref("meta.ENOENT"), ref("meta.foo")), tree), ShouldEqual, "FOO")
		})

		Convey("|| operator treats nil as a found value", func() {
			tree = YAML(`---
meta:
  foo: FOO
  bar: BAR
`)

			So(evaluate(or(null(), str("second")), tree), ShouldBeNil)
			So(evaluate(or(ref("meta.ENOENT"), null()), tree), ShouldBeNil)
		})
	})

	Convey("Expression Reduction Algorithm", t, func() {
		var orig, final *Expr
		var err error

		Convey("ignores singleton expression", func() {
			orig = str("string")
			final, err = orig.Reduce()
			So(err, ShouldBeNil)
			exprOk(final, orig)

			orig = null()
			final, err = orig.Reduce()
			So(err, ShouldBeNil)
			exprOk(final, orig)

			orig = ref("meta.key")
			final, err = orig.Reduce()
			So(err, ShouldBeNil)
			exprOk(final, orig)
		})

		Convey("handles normal alternates that terminated in a literal", func() {
			orig = or(ref("a.b.c"), str("default"))
			final, err = orig.Reduce()
			So(err, ShouldBeNil)
			exprOk(final, orig)
		})

		Convey("throws errors (warnings) for unreachable alternates", func() {
			orig = or(null(), str("ignored"))
			final, err = orig.Reduce()
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, `literal nil short-circuits expression (nil || "ignored")`)
			exprOk(final, null())

			orig = or(ref("some.key"), or(str("default"), ref("ignored.key")))
			final, err = orig.Reduce()
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, `literal "default" short-circuits expression (some.key || "default" || ignored.key)`)
			exprOk(final, or(ref("some.key"), str("default")))

			orig = or(or(ref("some.key"), str("default")), ref("ignored.key"))
			final, err = orig.Reduce()
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, `literal "default" short-circuits expression (some.key || "default" || ignored.key)`)
			exprOk(final, or(ref("some.key"), str("default")))
		})
	})

	Convey("File Operator", t, func() {
		op := FileOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`meta:
  sample_file: assets/file_operator/sample.txt
`),
		}
		basedir, _ := os.Getwd()

		Convey("can read a direct file", func() {
			r, err := op.Run(ev, []*Expr{
				str("assets/file_operator/test.txt"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "This is a test\n")
		})

		Convey("can read a file from a reference", func() {
			r, err := op.Run(ev, []*Expr{
				ref("meta.sample_file"),
			})

			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)

			content, err := ioutil.ReadFile("assets/file_operator/sample.txt")
			So(r.Value.(string), ShouldEqual, string(content))
		})

		Convey("can read a file relative to a specified base path", func() {
			os.Setenv("SPRUCE_FILE_BASE_PATH", filepath.Join(basedir, "assets/file_operator"))
			r, err := op.Run(ev, []*Expr{
				str("test.txt"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "This is a test\n")
		})

		if _, err := os.Stat("/etc/hosts"); err == nil {
			Convey("can read an absolute path", func() {
				os.Setenv("SPRUCE_FILE_BASE_PATH", filepath.Join(basedir, "assets/file_operator"))
				r, err := op.Run(ev, []*Expr{
					str("/etc/hosts"),
				})
				So(err, ShouldBeNil)
				So(r, ShouldNotBeNil)

				So(r.Type, ShouldEqual, Replace)

				content, err := ioutil.ReadFile("/etc/hosts")
				So(r.Value.(string), ShouldEqual, string(content))
			})
		}

		Convey("can handle a missing file", func() {
			r, err := op.Run(ev, []*Expr{
				str("no_one_should_ever_name_a_file_that_doesnt_exist_this_name"),
			})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

	})

	Convey("Grab Operator", t, func() {
		op := GrabOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`key:
  subkey:
    value: found it
    other: value 2
  list1:
    - first
    - second
  list2:
    - third
    - fourth
  lonely:
    - one
`),
		}

		Convey("can grab a single value", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.subkey.value"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "found it")
		})

		Convey("can grab a single list value", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.lonely"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)

			l, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)

			So(len(l), ShouldEqual, 1)
			So(l[0], ShouldEqual, "one")
		})

		Convey("can grab a multiple lists and flatten them", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.list1"),
				ref("key.lonely"),
				ref("key.list2"),
				ref("key.lonely.0"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)

			l, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)

			So(len(l), ShouldEqual, 6)
			So(l[0], ShouldEqual, "first")
			So(l[1], ShouldEqual, "second")
			So(l[2], ShouldEqual, "one")
			So(l[3], ShouldEqual, "third")
			So(l[4], ShouldEqual, "fourth")
			So(l[5], ShouldEqual, "one")
		})

		Convey("can grab multiple values", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.subkey.value"),
				ref("key.subkey.other"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 2)
			So(v[0], ShouldEqual, "found it")
			So(v[1], ShouldEqual, "value 2")
		})

		Convey("flattens constituent arrays", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.list2"),
				ref("key.list1"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 4)
			So(v[0], ShouldEqual, "third")
			So(v[1], ShouldEqual, "fourth")
			So(v[2], ShouldEqual, "first")
			So(v[3], ShouldEqual, "second")
		})

		Convey("throws errors for missing arguments", func() {
			_, err := op.Run(ev, []*Expr{})
			So(err, ShouldNotBeNil)
		})

		Convey("throws errors for dangling references", func() {
			_, err := op.Run(ev, []*Expr{
				ref("key.that.does.not.exist"),
			})
			So(err, ShouldNotBeNil)
		})
	})

	Convey("Environment Variable Resolution (via grab)", t, func() {
		op := GrabOperator{}
		ev := &Evaluator{}
		os.Setenv("GRAB_ONE", "one")
		os.Setenv("GRAB_TWO", "two")
		os.Setenv("GRAB_NOT", "")

		Convey("can grab a single environment value", func() {
			r, err := op.Run(ev, []*Expr{
				env("GRAB_ONE"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "one")
		})

		Convey("tries alternates until it finds a set environment variable", func() {
			r, err := op.Run(ev, []*Expr{
				or(env("GRAB_THREE"), or(env("GRAB_TWO"), env("GRAB_ONE"))),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "two")
		})

		Convey("throws errors for unset environment variables", func() {
			_, err := op.Run(ev, []*Expr{
				env("GRAB_NOT"),
			})
			So(err, ShouldNotBeNil)
		})
	})

	Convey("Concat Operator", t, func() {
		op := ConcatOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`key:
  subkey:
    value: found it
    other: value 2
  list1:
    - first
    - second
  list2:
    - third
    - fourth
douglas:
  adams: 42
math:
  PI: 3.14159
`),
		}

		Convey("can concat a single value", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.subkey.value"),
				ref("key.list1.0"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "found itfirst")
		})

		Convey("can concat a literal values", func() {
			r, err := op.Run(ev, []*Expr{
				str("a literal "),
				str("value"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "a literal value")
		})

		Convey("can concat multiple values", func() {
			r, err := op.Run(ev, []*Expr{
				str("I "),
				ref("key.subkey.value"),
				str("!"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "I found it!")
		})

		Convey("can concat integer literals", func() {
			r, err := op.Run(ev, []*Expr{
				str("the answer = "),
				ref("douglas.adams"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "the answer = 42")
		})

		Convey("can concat float literals", func() {
			r, err := op.Run(ev, []*Expr{
				ref("math.PI"),
				str(" is PI"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "3.14159 is PI")
		})

		Convey("throws errors for missing arguments", func() {
			_, err := op.Run(ev, []*Expr{})
			So(err, ShouldNotBeNil)

			_, err = op.Run(ev, []*Expr{str("one")})
			So(err, ShouldNotBeNil)
		})

		Convey("throws errors for dangling references", func() {
			_, err := op.Run(ev, []*Expr{
				ref("key.that.does.not.exist"),
				str("string"),
			})
			So(err, ShouldNotBeNil)
		})
	})

	Convey("static_ips Operator", t, func() {
		op := StaticIPOperator{}
		Reset(func() {
			UsedIPs = map[string]string{}
		})

		Convey("can resolve valid networks inside of job contexts", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 3)
			So(v[0], ShouldEqual, "10.0.0.5")
			So(v[1], ShouldEqual, "10.0.0.6")
			So(v[2], ShouldEqual, "10.0.0.7")
		})
		Convey("works with new style bosh manifests", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.5 - 10.0.0.10 ]
instance_groups:
- name: job1
  instances: 2
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 2)
			So(v[0], ShouldEqual, "10.0.0.5")
			So(v[1], ShouldEqual, "10.0.0.6")
		})

		Convey("works with multiple subnets", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.2 - 10.0.0.3 ]
  - static: [ 10.0.1.5 - 10.0.1.10 ]
instance_groups:
- name: job1
  instances: 4
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2), num(3)})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 4)
			So(v[0], ShouldEqual, "10.0.0.2")
			So(v[1], ShouldEqual, "10.0.0.3")
			So(v[2], ShouldEqual, "10.0.1.5")
			So(v[3], ShouldEqual, "10.0.1.6")
		})

		Convey("works with multiple subnets with an availability zone", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.2 - 10.0.0.3 ]
    az: z2
  - static: [ 10.0.1.5 - 10.0.1.10 ]
    az: z1
instance_groups:
- name: job1
  instances: 4
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2), num(3)})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 4)
			So(v[0], ShouldEqual, "10.0.0.2")
			So(v[1], ShouldEqual, "10.0.0.3")
			So(v[2], ShouldEqual, "10.0.1.5")
			So(v[3], ShouldEqual, "10.0.1.6")
		})

		Convey("works with instance_group availability zones", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.2 - 10.0.0.3 ]
    az: z1
  - static: [ 10.0.1.5 - 10.0.1.10 ]
    az: z2
instance_groups:
- name: job1
  instances: 3
  azs: [z2]
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 3)
			So(v[0], ShouldEqual, "10.0.1.5")
			So(v[1], ShouldEqual, "10.0.1.6")
			So(v[2], ShouldEqual, "10.0.1.7")
		})

		Convey("works with directly specified availability zones", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.2 - 10.0.0.4 ]
    az: z1
  - static: [ 10.0.2.6 - 10.0.2.10 ]
    az: z2
instance_groups:
- name: job1
  instances: 6
  azs: [z1,z2]
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{
				str("z2:1"),
				num(0),
				str("z1:2"),
				str("z2:2"),
				num(1),
				str("z2:4"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 6)
			So(v[0], ShouldEqual, "10.0.2.7")
			So(v[1], ShouldEqual, "10.0.0.2")
			So(v[2], ShouldEqual, "10.0.0.4")
			So(v[3], ShouldEqual, "10.0.2.8")
			So(v[4], ShouldEqual, "10.0.0.3")
			So(v[5], ShouldEqual, "10.0.2.10")
		})

		Convey("throws an error if an unknown availability zone is used in operator", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.2 - 10.0.0.4 ]
    az: z1
  - static: [ 10.0.2.6 - 10.0.2.10 ]
    az: z2
instance_groups:
- name: job1
  instances: 2
  azs: [z1,z2]
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{
				str("z2:0"),
				str("z3:1"),
			})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if offset for an availability zone is out of bounds", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.1 - 10.0.0.5 ]
    az: z1
  - static: [ 10.0.2.1 - 10.0.2.5 ]
    az: z2
instance_groups:
- name: job1
  instances: 2
  azs: [z1,z2]
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{
				str("z1:4"),
				str("z1:5"),
			})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if an instance_group availability zone is not found in subnets", func() {
			ev := &Evaluator{
				Here: cursor("instance_groups.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
- name: test-net
  subnets:
  - static: [ 10.0.0.2 - 10.0.0.4 ]
    az: z1
  - static: [ 10.0.2.6 - 10.0.2.10 ]
    az: z2
instance_groups:
- name: job1
  instances: 2
  azs: [z1,z2,z3]
  networks:
  - name: test-net
    static_ips: <------------- HERE ------------
`),
			}

			r, err := op.Run(ev, []*Expr{
				str("z1:0"),
				str("z2:1"),
			})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("can resolve valid large networks inside of job contexts", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.0 - 10.1.0.1 ]
jobs:
  - name: job1
    instances: 7
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{
				num(0),
				num(255),   // 2^8 - 1
				num(256),   // 2^8
				num(257),   // 2^8 + 1
				num(65535), // 2^16 - 1
				num(65536), // 2^16
				num(65537), // 2^16 + 1

				// 1st octet rollover testing disabled due to improve speed.
				// but verified working on 11/30/2015 - gfranks
				//				num(16777215), // 2^24 - 1
				//				num(16777216), // 2^24
				//				num(16777217), // 2^24 + 1
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 7)
			So(v[0], ShouldEqual, "10.0.0.0")
			So(v[1], ShouldEqual, "10.0.0.255")
			So(v[2], ShouldEqual, "10.0.1.0") //  3rd octet rollover
			So(v[3], ShouldEqual, "10.0.1.1")

			So(v[4], ShouldEqual, "10.0.255.255")
			So(v[5], ShouldEqual, "10.1.0.0") //  2nd octet rollover
			So(v[6], ShouldEqual, "10.1.0.1")

			// 1st octet rollover testing disabled due to improve speed.
			// but verified working on 11/30/2015 - gfranks
			//			So(v[7], ShouldEqual, "10.255.255.255")
			//			So(v[8], ShouldEqual, "11.0.0.0") //  1st octet rollover
			//			So(v[9], ShouldEqual, "11.0.0.1")
		})

		Convey("throws an error if no job name is specified", func() {
			ev := &Evaluator{
				Here: cursor("jobs.0.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if no job instances specified", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if job instances is not a number", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    instances: PI
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if job has no network name", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    instances: 3
    networks:
      - static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has no subnets key", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has no subnets", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets: []
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has no static ranges", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
     - {}
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has malformed static range array(s)", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
    - static: [ 10.0.0.1, 10.0.0.254 ]
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network static range has malformed IP addresses", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
    - static: 10.0.0.0.0.0.0.1 - geoff
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if the static address pool is too small", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
    - static: 172.16.31.10 - 172.16.31.11
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if the address pool ends before it starts", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
    - static: [ 10.8.0.1 - 10.0.0.255 ]
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []*Expr{num(0), num(1), num(2)})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "ends before it starts")
			So(r, ShouldBeNil)
		})
	})

	Convey("inject Operator", t, func() {
		op := InjectOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`key:
  subkey:
    value: found it
    other: value 2
  subkey2:
    value: overridden
    third: trois
  list1:
    - first
    - second
  list2:
    - third
    - fourth
`),
		}

		Convey("can inject a single sub-map", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.subkey"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Inject)
			v, ok := r.Value.(map[interface{}]interface{})
			So(ok, ShouldBeTrue)
			So(v["value"], ShouldEqual, "found it")
			So(v["other"], ShouldEqual, "value 2")
		})

		Convey("can inject multiple sub-maps", func() {
			r, err := op.Run(ev, []*Expr{
				ref("key.subkey"),
				ref("key.subkey2"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Inject)
			v, ok := r.Value.(map[interface{}]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 3)
			So(v["value"], ShouldEqual, "overridden")
			So(v["other"], ShouldEqual, "value 2")
			So(v["third"], ShouldEqual, "trois")
		})

		Convey("handles non-existent references", func() {
			_, err := op.Run(ev, []*Expr{
				ref("key.subkey"),
				ref("key.subkey2"),
				ref("key.subkey2.ENOENT"),
			})
			So(err, ShouldNotBeNil)
		})

		Convey("throws an error when trying to inject a scalar", func() {
			_, err := op.Run(ev, []*Expr{
				ref("key.subkey.value"),
			})
			So(err, ShouldNotBeNil)
		})

		Convey("throws an error when trying to inject a list", func() {
			_, err := op.Run(ev, []*Expr{
				ref("key.list1"),
			})
			So(err, ShouldNotBeNil)
		})
	})

	Convey("param Operator", t, func() {
		op := ParamOperator{}
		ev := &Evaluator{}

		Convey("always causes an error", func() {
			r, err := op.Run(ev, []*Expr{
				str("this is the error"),
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldEqual, "this is the error")
			So(r, ShouldBeNil)
		})
	})

	Convey("Join Operator", t, func() {
		op := JoinOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`---
meta:
  authorities:
  - password.write
  - clients.write
  - clients.read
  - scim.write
  - scim.read
  - uaa.admin
  - clients.secret

  secondlist:
  - admin.write
  - admin.read

  emptylist: []

  anotherkey:
  - entry1
  - somekey: value
  - entry2

  somestanza:
    foo: bar
    wom: bat
`),
		}

		Convey("can join a simple list", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.authorities"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret")
		})

		Convey("can join multiple lists", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.authorities"),
				ref("meta.secondlist"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret,admin.write,admin.read")
		})

		Convey("can join an empty list", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.emptylist"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "")
		})

		Convey("can join string literals", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				str("password.write"),
				str("clients.write"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "password.write,clients.write")
		})

		Convey("can join integer literals", func() {
			r, err := op.Run(ev, []*Expr{
				str(":"),
				num(4), num(8), num(15),
				num(16), num(23), num(42),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "4:8:15:16:23:42")
		})

		Convey("can join referenced string entry", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.somestanza.foo"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "bar")
		})

		Convey("can join referenced string entries", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.somestanza.foo"),
				ref("meta.somestanza.wom"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "bar,bat")
		})

		Convey("can join multiple referenced entries", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.authorities"),
				ref("meta.somestanza.foo"),
				ref("meta.somestanza.wom"),
				str("ending"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "password.write,clients.write,clients.read,scim.write,scim.read,uaa.admin,clients.secret,bar,bat,ending")
		})

		Convey("throws an error when there are no arguments", func() {
			r, err := op.Run(ev, []*Expr{})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "no arguments specified")
			So(r, ShouldBeNil)
		})

		Convey("throws an error when there are too few arguments", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "too few arguments supplied")
			So(r, ShouldBeNil)
		})

		Convey("throws an error when seperator argument is not a literal", func() {
			r, err := op.Run(ev, []*Expr{
				ref("meta.emptylist"),
				ref("meta.authorities"),
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "join operator only accepts literal argument for the seperator")
			So(r, ShouldBeNil)
		})

		Convey("throws an error when referenced entry is not a list or literal", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.somestanza"),
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "referenced entry is not a list or string")
			So(r, ShouldBeNil)
		})

		Convey("throws an error when referenced list contains non-string entries", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.anotherkey"),
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "is not compatible for")
			So(r, ShouldBeNil)
		})

		Convey("throws an error when there are unresolvable references", func() {
			r, err := op.Run(ev, []*Expr{
				str(","),
				ref("meta.non-existent"),
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "Unable to resolve")
			So(r, ShouldBeNil)
		})

		Convey("calculates dependencies correctly", func() {

			//TODO: Move this to a higher scope when more dependencies tests are added
			shouldHaveDeps := func(actual interface{}, expected ...interface{}) string {
				deps := actual.([]*tree.Cursor)
				paths := []string{}
				for _, path := range expected {
					normalizedPath, err := tree.ParseCursor(path.(string))
					if err != nil {
						panic(fmt.Sprintf("improper path %s passed to test", path.(string)))
					}
					paths = append(paths, normalizedPath.String())
				}
				actualPaths := []string{}
				//make an array so we can give some coherent output on error
				for _, dep := range deps {
					//Pass through tree so that tests can tolerate changes to the cursor lib
					actualPaths = append(actualPaths, dep.String())
				}
				//sort and compare
				sort.Strings(actualPaths)
				sort.Strings(paths)
				match := reflect.DeepEqual(actualPaths, paths)
				//give result
				if !match {
					return fmt.Sprintf("actual: %+v\n expected: %+v", actualPaths, paths)
				}
				return ""
			}

			Convey("with a single list", func() {
				deps := op.Dependencies(ev, []*Expr{
					str(" "),
					ref("meta.secondlist"),
				}, nil)
				So(deps, shouldHaveDeps, "meta.secondlist.[0]", "meta.secondlist.[1]")
			})

			Convey("with multiple lists", func() {
				deps := op.Dependencies(ev, []*Expr{
					str(" "),
					ref("meta.authorities"),
					ref("meta.secondlist"),
				}, nil)
				So(deps, shouldHaveDeps, "meta.authorities.[0]", "meta.authorities.[1]",
					"meta.authorities.[2]", "meta.authorities.[3]", "meta.authorities.[4]",
					"meta.authorities.[5]", "meta.authorities.[6]",
					"meta.secondlist.[0]", "meta.secondlist.[1]")
			})

			Convey("with a reference string", func() {
				deps := op.Dependencies(ev, []*Expr{
					str(" "),
					ref("meta.somestanza.foo"),
				}, nil)
				So(deps, shouldHaveDeps, "meta.somestanza.foo")
			})

			Convey("with multiple reference strings", func() {
				deps := op.Dependencies(ev, []*Expr{
					str(" "),
					ref("meta.somestanza.foo"),
					ref("meta.somestanza.wom"),
				}, nil)
				So(deps, shouldHaveDeps, "meta.somestanza.foo", "meta.somestanza.wom")
			})

			Convey("with a reference string and a list", func() {
				deps := op.Dependencies(ev, []*Expr{
					str(" "),
					ref("meta.somestanza.foo"),
					ref("meta.secondlist"),
				}, nil)
				So(deps, shouldHaveDeps, "meta.somestanza.foo", "meta.secondlist.[0]",
					"meta.secondlist.[1]")
			})

			Convey("with a literal string", func() {
				deps := op.Dependencies(ev, []*Expr{
					str(" "),
					str("literally literal"),
				}, nil)
				So(deps, shouldHaveDeps)
			})

			Convey("with a literal string and a reference string", func() {
				deps := op.Dependencies(ev, []*Expr{
					str(" "),
					str("beep"),
					ref("meta.somestanza.foo"),
				}, nil)
				So(deps, shouldHaveDeps, "meta.somestanza.foo")
			})
		})
	})

	Convey("empty operator", t, func() {
		op := EmptyOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`---
meta:
  authorities: meep
`),
		}

		//These three are with unquoted arguments (references)
		Convey("can replace with a hash", func() {
			r, err := op.Run(ev, []*Expr{
				ref("hash"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			val, isHash := r.Value.(map[string]interface{})
			So(isHash, ShouldBeTrue)
			So(val, ShouldResemble, map[string]interface{}{})
		})

		Convey("can replace with an array", func() {
			r, err := op.Run(ev, []*Expr{
				ref("array"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			val, isArray := r.Value.([]interface{})
			So(isArray, ShouldBeTrue)
			So(val, ShouldResemble, []interface{}{})
		})

		Convey("can replace with an empty string", func() {
			r, err := op.Run(ev, []*Expr{
				ref("string"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			val, isString := r.Value.(string)
			So(isString, ShouldBeTrue)
			So(val, ShouldEqual, "")
		})

		Convey("throws an error for unrecognized types", func() {
			r, err := op.Run(ev, []*Expr{
				ref("void"),
			})
			So(r, ShouldBeNil)
			So(err, ShouldNotBeNil)
		})

		Convey("works with string literals", func() {
			r, err := op.Run(ev, []*Expr{
				str("hash"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)
			So(r.Type, ShouldEqual, Replace)
			val, isHash := r.Value.(map[string]interface{})
			So(isHash, ShouldBeTrue)
			So(val, ShouldResemble, map[string]interface{}{})
		})

		Convey("throws an error with no args", func() {
			r, err := op.Run(ev, []*Expr{})
			So(r, ShouldBeNil)
			So(err, ShouldNotBeNil)
		})

		Convey("throws an error with too many args", func() {
			r, err := op.Run(ev, []*Expr{
				ref("hash"),
				ref("array"),
			})
			So(r, ShouldBeNil)
			So(err, ShouldNotBeNil)
		})

	})

}
Ejemplo n.º 5
0
func TestOperators(t *testing.T) {
	cursor := func(s string) *Cursor {
		c, err := ParseCursor(s)
		So(err, ShouldBeNil)
		return c
	}

	YAML := func(s string) map[interface{}]interface{} {
		y, err := simpleyaml.NewYaml([]byte(s))
		So(err, ShouldBeNil)

		data, err := y.Map()
		So(err, ShouldBeNil)

		return data
	}

	Convey("Parser", t, func() {
		Convey("parses op calls in their entirety", func() {

			opOk := func(code string, name string, args ...interface{}) {
				op, err := ParseOpcall(code)
				So(err, ShouldBeNil)
				So(op, ShouldNotBeNil)

				_, ok := op.op.(NullOperator)
				So(ok, ShouldBeTrue)
				So(op.op.(NullOperator).Missing, ShouldEqual, name)

				So(len(op.args), ShouldEqual, len(args))
				for i, expect := range args {
					switch expect.(type) {
					case string:
						So(op.args[i], ShouldEqual, expect.(string))

					case *Cursor:
						_, ok := op.args[i].(*Cursor)
						So(ok, ShouldBeTrue)
						So(op.args[i].(*Cursor).String(), ShouldEqual, expect.(*Cursor).String())
					}
				}
			}

			cursor := func(s string) *Cursor {
				c, err := ParseCursor(s)
				So(err, ShouldBeNil)
				return c
			}

			Convey("handles opcodes with and without arguments", func() {
				opOk(`(( null ))`, "null")
				opOk(`(( null 42 ))`, "null", "42")
				opOk(`(( null 1 2 3 4 ))`, "null", "1", "2", "3", "4")
			})

			Convey("ignores optional whitespace", func() {
				opOk(`((null))`, "null")
				opOk(`((	null	))`, "null")
				opOk(`((  	null  	))`, "null")

				args := []interface{}{"1", "2", "3"}
				opOk(`((null 1 2 3))`, "null", args...)
				opOk(`((null 1	2	3))`, "null", args...)
				opOk(`((null 1	2	3	))`, "null", args...)
				opOk(`((null 1 	 2 	 3 	 ))`, "null", args...)
			})

			Convey("allows use of commas to separate arguments", func() {
				args := []interface{}{"1", "2", "3"}
				opOk(`((null 1, 2, 3))`, "null", args...)
				opOk(`((null 1,	2,	3))`, "null", args...)
				opOk(`((null 1,	2,	3,	))`, "null", args...)
				opOk(`((null 1 ,	 2 ,	 3 ,	 ))`, "null", args...)
			})

			Convey("allows use of parentheses around arguments", func() {
				args := []interface{}{"1", "2", "3"}
				opOk(`((null(1,2,3)))`, "null", args...)
				opOk(`((null(1, 2, 3) ))`, "null", args...)
				opOk(`((null( 1,	2,	3)))`, "null", args...)
				opOk(`((null (1,	2,	3)	))`, "null", args...)
				opOk(`((null (1 ,	 2 ,	 3)	 ))`, "null", args...)
			})

			Convey("handles string literal arguments", func() {
				opOk(`(( null "string" ))`, "null", "string")
				opOk(`(( null "string with whitespace" ))`, "null", "string with whitespace")
				opOk(`(( null "a \"quoted\" string" ))`, "null", `a "quoted" string`)
				opOk(`(( null "\\escaped" ))`, "null", `\escaped`)
			})

			Convey("handles reference (cursor) arguments", func() {
				opOk(`(( null x.y.z ))`, "null", cursor("x.y.z"))
				opOk(`(( null x.[0].z ))`, "null", cursor("x.0.z"))
				opOk(`(( null x[0].z ))`, "null", cursor("x.0.z"))
				opOk(`(( null x[0]z ))`, "null", cursor("x.0.z"))
			})

			Convey("handles mixed collections of argument types", func() {
				opOk(`(( xyzzy "string" x.y.z 42  ))`, "xyzzy", "string", cursor("x.y.z"), "42")
				opOk(`(( xyzzy("string" x.y.z 42) ))`, "xyzzy", "string", cursor("x.y.z"), "42")
			})
		})
	})

	Convey("Grab Operator", t, func() {
		op := GrabOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`key:
  subkey:
    value: found it
    other: value 2
  list1:
    - first
    - second
  list2:
    - third
    - fourth
  lonely:
    - one
`),
		}

		Convey("can grab a single value", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.subkey.value"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "found it")
		})

		Convey("can grab a single list value", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.lonely"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)

			l, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)

			So(len(l), ShouldEqual, 1)
			So(l[0], ShouldEqual, "one")
		})

		Convey("can grab a multiple lists and flatten them", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.list1"),
				cursor("key.lonely"),
				cursor("key.list2"),
				cursor("key.lonely.0"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)

			l, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)

			So(len(l), ShouldEqual, 6)
			So(l[0], ShouldEqual, "first")
			So(l[1], ShouldEqual, "second")
			So(l[2], ShouldEqual, "one")
			So(l[3], ShouldEqual, "third")
			So(l[4], ShouldEqual, "fourth")
			So(l[5], ShouldEqual, "one")
		})

		Convey("can grab multiple values", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.subkey.value"),
				cursor("key.subkey.other"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 2)
			So(v[0], ShouldEqual, "found it")
			So(v[1], ShouldEqual, "value 2")
		})

		Convey("flattens constituent arrays", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.list2"),
				cursor("key.list1"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 4)
			So(v[0], ShouldEqual, "third")
			So(v[1], ShouldEqual, "fourth")
			So(v[2], ShouldEqual, "first")
			So(v[3], ShouldEqual, "second")
		})

		Convey("throws errors for missing arguments", func() {
			_, err := op.Run(ev, []interface{}{})
			So(err, ShouldNotBeNil)
		})

		Convey("throws errors for dangling references", func() {
			_, err := op.Run(ev, []interface{}{
				cursor("key.that.does.not.exist"),
			})
			So(err, ShouldNotBeNil)
		})
	})

	Convey("Concat Operator", t, func() {
		op := ConcatOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`key:
  subkey:
    value: found it
    other: value 2
  list1:
    - first
    - second
  list2:
    - third
    - fourth
douglas:
  adams: 42
math:
  PI: 3.14159
`),
		}

		Convey("can concat a single value", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.subkey.value"),
				cursor("key.list1.0"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "found itfirst")
		})

		Convey("can concat a literal values", func() {
			r, err := op.Run(ev, []interface{}{
				"a literal ",
				"value",
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "a literal value")
		})

		Convey("can concat multiple values", func() {
			r, err := op.Run(ev, []interface{}{
				"I ",
				cursor("key.subkey.value"),
				"!",
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "I found it!")
		})

		Convey("can concat integer literals", func() {
			r, err := op.Run(ev, []interface{}{
				"the answer = ",
				cursor("douglas.adams"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "the answer = 42")
		})

		Convey("can concat float literals", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("math.PI"),
				" is PI",
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			So(r.Value.(string), ShouldEqual, "3.14159 is PI")
		})

		Convey("throws errors for missing arguments", func() {
			_, err := op.Run(ev, []interface{}{})
			So(err, ShouldNotBeNil)

			_, err = op.Run(ev, []interface{}{"one"})
			So(err, ShouldNotBeNil)
		})

		Convey("throws errors for dangling references", func() {
			_, err := op.Run(ev, []interface{}{
				cursor("key.that.does.not.exist"),
				"string",
			})
			So(err, ShouldNotBeNil)
		})
	})

	Convey("static_ips Operator", t, func() {
		op := StaticIPOperator{}

		Convey("can resolve valid networks inside of job contexts", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Replace)
			v, ok := r.Value.([]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 3)
			So(v[0], ShouldEqual, "10.0.0.5")
			So(v[1], ShouldEqual, "10.0.0.6")
			So(v[2], ShouldEqual, "10.0.0.7")
		})

		Convey("throws an error if no job name is specified", func() {
			ev := &Evaluator{
				Here: cursor("jobs.0.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if no job instances specified", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if job instances is not a number", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    instances: PI
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if job has no network name", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
      - static: [ 10.0.0.5 - 10.0.0.10 ]
jobs:
  - name: job1
    instances: 3
    networks:
      - static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has no subnets key", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has no subnets", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets: []
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has no static ranges", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
     - {}
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network has malformed static range array(s)", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
    - static: [ 10.0.0.1, 10.0.0.254 ]
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if network static range has malformed IP addresses", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
    - static: 10.0.0.0.0.0.0.1 - geoff
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})

		Convey("throws an error if the static address pool is too small", func() {
			ev := &Evaluator{
				Here: cursor("jobs.job1.networks.0.static_ips"),
				Tree: YAML(
					`networks:
  - name: test-network
    subnets:
    - static: 172.16.31.10 - 172.16.31.11
jobs:
  - name: job1
    instances: 3
    networks:
      - name: test-network
        static_ips: <---------- HERE -----------------
`),
			}

			r, err := op.Run(ev, []interface{}{"0", "1", "2"})
			So(err, ShouldNotBeNil)
			So(r, ShouldBeNil)
		})
	})

	Convey("inject Operator", t, func() {
		op := InjectOperator{}
		ev := &Evaluator{
			Tree: YAML(
				`key:
  subkey:
    value: found it
    other: value 2
  subkey2:
    value: overridden
    third: trois
  list1:
    - first
    - second
  list2:
    - third
    - fourth
`),
		}

		Convey("can inject a single sub-map", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.subkey"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Inject)
			v, ok := r.Value.(map[interface{}]interface{})
			So(ok, ShouldBeTrue)
			So(v["value"], ShouldEqual, "found it")
			So(v["other"], ShouldEqual, "value 2")
		})

		Convey("can inject multiple sub-maps", func() {
			r, err := op.Run(ev, []interface{}{
				cursor("key.subkey"),
				cursor("key.subkey2"),
			})
			So(err, ShouldBeNil)
			So(r, ShouldNotBeNil)

			So(r.Type, ShouldEqual, Inject)
			v, ok := r.Value.(map[interface{}]interface{})
			So(ok, ShouldBeTrue)
			So(len(v), ShouldEqual, 3)
			So(v["value"], ShouldEqual, "overridden")
			So(v["other"], ShouldEqual, "value 2")
			So(v["third"], ShouldEqual, "trois")
		})

		Convey("handles non-existent references", func() {
			_, err := op.Run(ev, []interface{}{
				cursor("key.subkey"),
				cursor("key.subkey2"),
				cursor("key.subkey2.ENOENT"),
			})
			So(err, ShouldNotBeNil)
		})

		Convey("throws an error when trying to inject a scalar", func() {
			_, err := op.Run(ev, []interface{}{
				cursor("key.subkey.value"),
			})
			So(err, ShouldNotBeNil)
		})

		Convey("throws an error when trying to inject a list", func() {
			_, err := op.Run(ev, []interface{}{
				cursor("key.list1"),
			})
			So(err, ShouldNotBeNil)
		})
	})

	Convey("param Operator", t, func() {
		op := ParamOperator{}
		ev := &Evaluator{}

		Convey("always causes an error", func() {
			r, err := op.Run(ev, []interface{}{
				"this is the error",
			})
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldEqual, "this is the error")
			So(r, ShouldBeNil)
		})
	})
}
Ejemplo n.º 6
0
func TestVault(t *testing.T) {
	YAML := func(s string) map[interface{}]interface{} {
		y, err := simpleyaml.NewYaml([]byte(s))
		So(err, ShouldBeNil)

		data, err := y.Map()
		So(err, ShouldBeNil)

		return data
	}
	ToYAML := func(tree map[interface{}]interface{}) string {
		y, err := yaml.Marshal(tree)
		So(err, ShouldBeNil)
		return string(y)
	}
	ReYAML := func(s string) string {
		return ToYAML(YAML(s))
	}
	RunTests := func(src string) {
		var test, input, output string
		var current *string
		testPat := regexp.MustCompile(`^##+\s+(.*)\s*$`)

		convey := func() {
			if test != "" {
				Convey(test, func() {
					ev := &Evaluator{Tree: YAML(input)}
					err := ev.RunPhase(EvalPhase)
					So(err, ShouldBeNil)
					So(ToYAML(ev.Tree), ShouldEqual, ReYAML(output))
				})
			}
		}

		s := bufio.NewScanner(strings.NewReader(src))
		for s.Scan() {
			if testPat.MatchString(s.Text()) {
				m := testPat.FindStringSubmatch(s.Text())
				convey()
				test, input, output = m[1], "", ""
				continue
			}

			if s.Text() == "---" {
				if input == "" {
					current = &input
				} else {
					current = &output
				}
				continue
			}

			if current != nil {
				*current = *current + s.Text() + "\n"
			}
		}
		convey()
	}

	RunErrorTests := func(src string) {
		var test, input, errors string
		var current *string
		testPat := regexp.MustCompile(`^##+\s+(.*)\s*$`)

		convey := func() {
			if test != "" {
				Convey(test, func() {
					ev := &Evaluator{Tree: YAML(input)}
					err := ev.RunPhase(EvalPhase)
					So(err, ShouldNotBeNil)
					So(strings.Trim(err.Error(), " \t"), ShouldEqual, errors)
				})
			}
		}

		s := bufio.NewScanner(strings.NewReader(src))
		for s.Scan() {
			if testPat.MatchString(s.Text()) {
				m := testPat.FindStringSubmatch(s.Text())
				convey()
				test, input, errors = m[1], "", ""
				continue
			}

			if s.Text() == "---" {
				if input == "" {
					current = &input
				} else {
					current = &errors
				}
				continue
			}

			if current != nil {
				*current = *current + s.Text() + "\n"
			}
		}
		convey()
	}

	Convey("Disconnected Vault", t, func() {
		os.Setenv("REDACT", "yes")

		RunTests(`
##################################################  emits REDACTED when asked to
---
secret: (( vault "secret/hand:shake" ))

---
secret: REDACTED
`)
	})

	Convey("Connected Vault", t, func() {
		mock := httptest.NewServer(
			http.HandlerFunc(
				func(w http.ResponseWriter, r *http.Request) {
					if r.Header.Get("X-Vault-Token") != "sekrit-toekin" {
						w.WriteHeader(403)
						fmt.Fprintf(w, `{"errors":["missing client token"]}`)
						return
					}
					switch r.URL.Path {
					case "/v1/secret/hand":
						w.WriteHeader(200)
						fmt.Fprintf(w, `{"data":{"shake":"knock, knock"}}`)
					case "/v1/secret/admin":
						w.WriteHeader(200)
						fmt.Fprintf(w, `{"data":{"username":"******","password":"******"}}`)
					case "/v1/secret/key":
						w.WriteHeader(200)
						fmt.Fprintf(w, `{"data":{"test":"testing"}}`)
					case "/v1/secret/malformed":
						w.WriteHeader(200)
						fmt.Fprintf(w, `wait, this isn't JSON`)
					case "/v1/secret/structure":
						w.WriteHeader(200)
						fmt.Fprintf(w, `{"data":{"data":[1,2,3]}}`)
					default:
						w.WriteHeader(404)
						fmt.Fprintf(w, `{"errors":[]}`)
					}
				},
			),
		)
		defer mock.Close()

		os.Setenv("REDACT", "")
		os.Setenv("VAULT_ADDR", mock.URL)
		os.Setenv("VAULT_TOKEN", "sekrit-toekin")
		RunTests(`
################################################  emits sensitive credentials
---
meta:
  prefix: secret
  key: secret/key:test
secret: (( vault "secret/hand:shake" ))
username: (( vault "secret/admin:username" ))
password: (( vault "secret/admin:password" ))
prefixed: (( vault meta.prefix "/admin:password" ))
key: (( vault $.meta.key ))

---
meta:
  key: secret/key:test
  prefix: secret
secret: knock, knock
username: admin
password: x12345
prefixed: x12345
key: testing
`)

		os.Setenv("VAULT_ADDR", mock.URL)
		oldhome := os.Getenv("HOME")
		os.Setenv("HOME", "assets/home/auth")
		os.Setenv("VAULT_TOKEN", "")
		RunTests(`
##########################  retrieves token transparently from ~/.vault-token
---
secret: (( vault "secret/hand:shake" ))

---
secret: knock, knock
`)

		os.Setenv("VAULT_ADDR", "garbage")
		os.Setenv("VAULT_TOKEN", "")
		os.Setenv("HOME", "assets/home/svtoken")
		ioutil.WriteFile("assets/home/svtoken/.svtoken",
			[]byte("vault: "+mock.URL+"\n"+
				"token: sekrit-toekin\n"), 0644)
		RunTests(`
##############################  retrieves token transparently from ~/.svtoken
---
secret: (( vault "secret/hand:shake" ))

---
secret: knock, knock
`)

		/* RESET TO A VALID, AUTHENTICATED STATE */
		os.Setenv("VAULT_ADDR", mock.URL)
		os.Setenv("HOME", "assets/home/auth")

		RunErrorTests(`
#########################################  fails when missing its argument
---
secret: (( vault ))

---
1 error(s) detected:
 - $.secret: vault operator requires at least one argument

#########################################  fails on non-existent reference
---
meta: {}
secret: (( vault $.meta.key ))

---
1 error(s) detected:
 - $.secret: Unable to resolve ` + "`" + `meta.key` + "`" + `: ` + "`" + `$.meta.key` + "`" + ` could not be found in the datastructure

####################################################  fails on map reference
---
meta:
  key: secret/hand:shake
secret: (( vault $.meta ))

---
1 error(s) detected:
 - $.secret: tried to look up $.meta, which is not a string scalar

##################################################  fails on list reference
---
meta:
  - first
secret: (( vault $.meta ))

---
1 error(s) detected:
 - $.secret: tried to look up $.meta, which is not a string scalar

#########################################  fails on non-existent credentials
---
secret: (( vault "secret/e:noent" ))

---
1 error(s) detected:
 - $.secret: secret secret/e:noent not found

##############################################  fails on non-string argument
---
secret: (( vault 42 ))

---
1 error(s) detected:
 - $.secret: invalid argument 42; must be in the form path/to/secret:key

#################################################  fails on non-JSON response
---
secret: (( vault "secret/malformed:key" ))

---
1 error(s) detected:
 - $.secret: bad JSON response received from Vault: "wait, this isn't JSON"

#################################################  fails on non-string data
---
secret: (( vault "secret/structure:data" ))

---
1 error(s) detected:
 - $.secret: secret secret/structure:data is not a string

`)

		os.Setenv("VAULT_TOKEN", "incorrect")
		RunErrorTests(`
#####################################################  fails on a bad token
---
secret: (( vault "secret/hand:shake" ))

---
1 error(s) detected:
 - $.secret: failed to retrieve secret/hand:shake from Vault (` + os.Getenv("VAULT_ADDR") + `): missing client token

`)

		oldhome = os.Getenv("HOME")
		os.Setenv("HOME", "assets/home/unauth")
		os.Setenv("REDACT", "")
		os.Setenv("VAULT_TOKEN", "")
		RunErrorTests(`
################################################  fails on a missing token
---
secret: (( vault "secret/hand:shake" ))

---
1 error(s) detected:
 - $.secret: Failed to determine Vault URL / token, and the $REDACT environment variable is not set.

`)
		os.Setenv("HOME", oldhome)
	})

	Convey("It correctly parses path", t, func() {
		for _, test := range []struct {
			path      string //The full path to run through the parse function
			expSecret string //What is expected to be left of the colon
			expKey    string //What is expected to be right of the colon
		}{
			//-----TEST CASES GO HERE-----
			// { "path to parse", "expected secret", "expected key" }
			{"just/a/secret", "just/a/secret", ""},
			{"secret/with/colon:", "secret/with/colon", ""},
			{":", "", ""},
			{"a:", "a", ""},
			{"", "", ""},
			{"secret/and:key", "secret/and", "key"},
			{":justakey", "", "justakey"},
			{"secretwithcolon://127.0.0.1:", "secretwithcolon://127.0.0.1", ""},
			{"secretwithcolons://127.0.0.1:8500:", "secretwithcolons://127.0.0.1:8500", ""},
			{"secretwithcolons://127.0.0.1:8500:andkey", "secretwithcolons://127.0.0.1:8500", "andkey"},
		} {
			Convey(test.path, func() {
				secret, key := parsePath(test.path)
				So(secret, ShouldEqual, test.expSecret)
				So(key, ShouldEqual, test.expKey)
			})
		}
	})
}
Ejemplo n.º 7
0
func TestExamples(t *testing.T) {
	var stdout string
	printfStdOut = func(format string, args ...interface{}) {
		stdout = fmt.Sprintf(format, args...)
	}
	var stderr string
	printfStdErr = func(format string, args ...interface{}) {
		stderr = fmt.Sprintf(format, args...)
	}

	rc := 256 // invalid return code to catch any issues
	exit = func(code int) {
		rc = code
	}

	YAML := func(path string) string {
		s, err := ioutil.ReadFile(path)
		So(err, ShouldBeNil)

		y, err := simpleyaml.NewYaml([]byte(s))
		So(err, ShouldBeNil)

		data, err := y.Map()
		So(err, ShouldBeNil)

		out, err := yaml.Marshal(data)
		So(err, ShouldBeNil)

		return string(out) + "\n"
	}

	Convey("Examples from README.md", t, func() {
		example := func(args ...string) {
			expect := args[len(args)-1]
			args = args[:len(args)-1]

			os.Args = []string{"spruce", "merge"}
			os.Args = append(os.Args, args...)
			stdout, stderr = "", ""
			main()

			So(stderr, ShouldEqual, "")
			So(stdout, ShouldEqual, YAML(expect))
		}

		Convey("Basic Example", func() {
			example(
				"../../examples/basic/main.yml",
				"../../examples/basic/merge.yml",

				"../../examples/basic/output.yml",
			)
		})

		Convey("Map Replacements", func() {
			example(
				"../../examples/map-replacement/original.yml",
				"../../examples/map-replacement/delete.yml",
				"../../examples/map-replacement/insert.yml",

				"../../examples/map-replacement/output.yml",
			)
		})

		Convey("Key Removal", func() {
			example(
				"--prune", "deleteme",
				"../../examples/key-removal/original.yml",
				"../../examples/key-removal/things.yml",

				"../../examples/key-removal/output.yml",
			)

			example(
				"../../examples/pruning/base.yml",
				"../../examples/pruning/jobs.yml",
				"../../examples/pruning/networks.yml",

				"../../examples/pruning/output.yml",
			)
		})

		Convey("Lists of Maps", func() {
			example(
				"../../examples/list-of-maps/original.yml",
				"../../examples/list-of-maps/new.yml",

				"../../examples/list-of-maps/output.yml",
			)
		})

		Convey("Static IPs", func() {
			example(
				"../../examples/static-ips/jobs.yml",
				"../../examples/static-ips/properties.yml",
				"../../examples/static-ips/networks.yml",

				"../../examples/static-ips/output.yml",
			)
		})

		Convey("Static IPs with availability zones", func() {
			example(
				"../../examples/availability-zones/jobs.yml",
				"../../examples/availability-zones/properties.yml",
				"../../examples/availability-zones/networks.yml",

				"../../examples/availability-zones/output.yml",
			)
		})

		Convey("Injecting Subtrees", func() {
			example(
				"--prune", "meta",
				"../../examples/inject/all-in-one.yml",

				"../../examples/inject/output.yml",
			)

			example(
				"--prune", "meta",
				"../../examples/inject/templates.yml",
				"../../examples/inject/green.yml",

				"../../examples/inject/output.yml",
			)
		})

		Convey("Pruning", func() {
			example(
				"../../examples/pruning/base.yml",
				"../../examples/pruning/jobs.yml",
				"../../examples/pruning/networks.yml",

				"../../examples/pruning/output.yml",
			)
		})

		Convey("Inserting", func() {
			example(
				"../../examples/inserting/main.yml",
				"../../examples/inserting/addon.yml",

				"../../examples/inserting/result.yml",
			)
		})
	})
}
Ejemplo n.º 8
0
func TestEvaluator(t *testing.T) {
	YAML := func(s string) map[interface{}]interface{} {
		y, err := simpleyaml.NewYaml([]byte(s))
		So(err, ShouldBeNil)

		data, err := y.Map()
		So(err, ShouldBeNil)

		return data
	}

	Convey("Evaluator", t, func() {
		Convey("Data Flow", func() {
			Convey("Generates a sequential list of operator calls, in order", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  foo: FOO
  bar:  (( grab meta.foo ))
  baz:  (( grab meta.bar ))
  quux: (( grab meta.baz ))
  boz:  (( grab meta.quux ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)
				So(ev.DataOps, ShouldNotBeNil)

				// expect: meta.bar   (( grab meta.foo ))
				//         meta.baz   (( grab meta.bar ))
				//         meta.quux  (( grab meta.baz ))
				//         meta.boz   (( grab meta.quux ))
				So(len(ev.DataOps), ShouldEqual, 4)

				So(ev.DataOps[0].where.String(), ShouldEqual, "meta.bar")
				So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.foo ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "meta.baz")
				So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.bar ))")

				So(ev.DataOps[2].where.String(), ShouldEqual, "meta.quux")
				So(ev.DataOps[2].src, ShouldEqual, "(( grab meta.baz ))")

				So(ev.DataOps[3].where.String(), ShouldEqual, "meta.boz")
				So(ev.DataOps[3].src, ShouldEqual, "(( grab meta.quux ))")
			})

			Convey("detects direct (a -> b -> a) cycles in data flow graph", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  bar: (( grab meta.foo ))
  foo: (( grab meta.bar ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldNotBeNil)
			})

			Convey("detects indirect (a -> b -> c -> a) cycles in data flow graph", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  foo: (( grab meta.bar ))
  bar: (( grab meta.baz ))
  baz: (( grab meta.foo ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldNotBeNil)
			})

			Convey("handles data flow regardless of operator type", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  foo: FOO
  bar: (( grab meta.foo ))
  baz: (( grab meta.bar ))
  quux: (( concat "literal:" meta.baz ))
  boz: (( grab meta.quux ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				// expect: meta.bar   (( grab meta.foo ))
				//         meta.baz   (( grab meta.bar ))
				//         meta.quux  (( concat "literal:" meta.baz ))
				//         meta.boz   (( grab meta.quux ))
				So(len(ev.DataOps), ShouldEqual, 4)

				So(ev.DataOps[0].where.String(), ShouldEqual, "meta.bar")
				So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.foo ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "meta.baz")
				So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.bar ))")

				So(ev.DataOps[2].where.String(), ShouldEqual, "meta.quux")
				So(ev.DataOps[2].src, ShouldEqual, `(( concat "literal:" meta.baz ))`)

				So(ev.DataOps[3].where.String(), ShouldEqual, "meta.boz")
				So(ev.DataOps[3].src, ShouldEqual, "(( grab meta.quux ))")
			})

			Convey("handles data flow for operators in lists", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  - FOO
  - (( grab meta.0 ))
  - (( grab meta.1 ))
  - (( concat "literal:" meta.2 ))
  - (( grab meta.3 ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				// expect: meta.1  (( grab meta.0 ))
				//         meta.2  (( grab meta.1 ))
				//         meta.3  (( concat "literal:" meta.2 ))
				//         meta.4  (( grab meta.3 ))
				So(len(ev.DataOps), ShouldEqual, 4)

				So(ev.DataOps[0].where.String(), ShouldEqual, "meta.1")
				So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.0 ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "meta.2")
				So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.1 ))")

				So(ev.DataOps[2].where.String(), ShouldEqual, "meta.3")
				So(ev.DataOps[2].src, ShouldEqual, `(( concat "literal:" meta.2 ))`)

				So(ev.DataOps[3].where.String(), ShouldEqual, "meta.4")
				So(ev.DataOps[3].src, ShouldEqual, "(( grab meta.3 ))")
			})

			Convey("handles deep copy in data flow graph", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  first: [ a, b, c ]
  second: (( grab meta.first ))
  third:  (( grab meta.second ))
  gotcha: (( grab meta.third.0 ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				// expect: meta.second (( grab meta.first ))
				//         meta.third  (( grab meta.second ))
				//         meta.gotcha (( grab meta.third.0 ))
				//
				//   (the key point here is that meta.third.0 doesn't
				//    exist in the tree until we start evaluating, but
				//    we still need to get the order correct; we should
				//    have a dep on meta.third, and hope that run-time
				//    resolution puts an array there for us to find...)
				//
				So(len(ev.DataOps), ShouldEqual, 3)

				So(ev.DataOps[0].where.String(), ShouldEqual, "meta.second")
				So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.first ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "meta.third")
				So(ev.DataOps[1].src, ShouldEqual, "(( grab meta.second ))")

				So(ev.DataOps[2].where.String(), ShouldEqual, "meta.gotcha")
				So(ev.DataOps[2].src, ShouldEqual, "(( grab meta.third.0 ))")
			})

			Convey("handles implicit static_ip dependency on jobs.*.networks.*.name", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  environment: prod
  size: 4
networks:
  - name: sandbox
    subnets:
    - static: [ 10.2.0.5 - 10.2.0.10 ]
  - name: prod
    subnets:
    - static: [ 10.0.0.5 - 10.0.0.100 ]
jobs:
  - name: job1
    instances: 4
    networks:
      - name: (( grab meta.environment ))
        static_ips: (( static_ips 1 2 3 4 ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				// expect: jobs.0.networks.0.name        (( grab meta.environment ))
				//         jobs.0.networks.0.static_ips  (( static_ips 1 2 3 4 ))
				So(len(ev.DataOps), ShouldEqual, 2)

				So(ev.DataOps[0].where.String(), ShouldEqual, "jobs.0.networks.0.name")
				So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.environment ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "jobs.0.networks.0.static_ips")
				So(ev.DataOps[1].src, ShouldEqual, "(( static_ips 1 2 3 4 ))")
			})

			Convey("handles implicit static_ip dependency on networks.*.name", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  net: real
  environment: prod
  size: 4
networks:
  - name: (( concat meta.net "-prod" ))
    subnets:
    - static: [ 10.0.0.5 - 10.0.0.100 ]
jobs:
  - name: job1
    instances: 4
    networks:
      - name: prod-net # must be literal to avoid non-determinism
        static_ips: (( static_ips 1 2 3 4 ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				// expect: networks.0.name               (( concat meta.net "-prod" ))
				//         jobs.0.networks.0.static_ips  (( static_ips 1 2 3 4 ))
				So(len(ev.DataOps), ShouldEqual, 2)

				So(ev.DataOps[0].where.String(), ShouldEqual, "networks.0.name")
				So(ev.DataOps[0].src, ShouldEqual, `(( concat meta.net "-prod" ))`)

				So(ev.DataOps[1].where.String(), ShouldEqual, "jobs.0.networks.0.static_ips")
				So(ev.DataOps[1].src, ShouldEqual, "(( static_ips 1 2 3 4 ))")
			})

			Convey("handles dependency on static_ips() calls via (( grab )) calls", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
networks:
  - name: net1
    subnets:
    - static: [ 10.0.0.5 - 10.0.0.100 ]

jobs:
  - name: job1
    instances: 4
    networks:
      - name: net1
        static_ips: (( static_ips 1 2 3 4 ))

properties:
  job_ips: (( grab jobs.job1.networks.net1.static_ips ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)
				So(ev.DataOps, ShouldNotBeNil)

				// expect: jobs.0.networks.0.static_ips   (( static_ips 1 2 3 4 ))
				//         properties.job_ips             (( grab jobs.job1.networks.net1.static_ips ))
				So(len(ev.DataOps), ShouldEqual, 2)

				So(ev.DataOps[0].where.String(), ShouldEqual, "jobs.0.networks.0.static_ips")
				So(ev.DataOps[0].src, ShouldEqual, "(( static_ips 1 2 3 4 ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "properties.job_ips")
				So(ev.DataOps[1].src, ShouldEqual, "(( grab jobs.job1.networks.net1.static_ips ))")
			})

			Convey("handles implicit deps on sub-tree operations in (( inject ... )) targets", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  template:
    foo: bar
    baz: (( grab meta.template.foo ))

thing:
  <<<: (( inject meta.template ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				So(len(ev.DataOps), ShouldEqual, 2)

				So(ev.DataOps[0].where.String(), ShouldEqual, "meta.template.baz")
				So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.template.foo ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "thing.<<<")
				So(ev.DataOps[1].src, ShouldEqual, "(( inject meta.template ))")
			})

			Convey("handles inject of an inject of a grab (so meta)", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  template1:
    foo: bar
    baz: (( grab meta.template1.foo ))
  template2:
    <<<: (( inject meta.template1 ))
    xyzzy: nothing happens

thing:
  <<<: (( inject meta.template2 ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				So(len(ev.DataOps), ShouldEqual, 3)

				So(ev.DataOps[0].where.String(), ShouldEqual, "meta.template1.baz")
				So(ev.DataOps[0].src, ShouldEqual, "(( grab meta.template1.foo ))")

				So(ev.DataOps[1].where.String(), ShouldEqual, "meta.template2.<<<")
				So(ev.DataOps[1].src, ShouldEqual, "(( inject meta.template1 ))")

				So(ev.DataOps[2].where.String(), ShouldEqual, "thing.<<<")
				So(ev.DataOps[2].src, ShouldEqual, "(( inject meta.template2 ))")
			})
		})

		Convey("Patching", func() {
			valueIs := func(ev *Evaluator, path string, expect string) {
				c, err := ParseCursor(path)
				So(err, ShouldBeNil)

				v, err := c.ResolveString(ev.Tree)
				So(err, ShouldBeNil)

				So(v, ShouldEqual, expect)
			}
			notPresent := func(ev *Evaluator, path string) {
				c, err := ParseCursor(path)
				So(err, ShouldBeNil)

				_, err = c.ResolveString(ev.Tree)
				So(err, ShouldNotBeNil)
				So(err.Error(), ShouldContainSubstring, "could not be found")
			}

			Convey("can handle simple map-based Replace actions", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  domain: sandbox.example.com
  web: (( grab meta.domain ))
urls:
  home: (( concat "http://www." meta.web ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "meta.web", "sandbox.example.com")
				valueIs(ev, "urls.home", "http://www.sandbox.example.com")
			})

			Convey("can handle Replacement actions where the new value is a list", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  things:
    - one
    - two
grocery:
  list: (( grab meta.things ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "grocery.list.0", "one")
				valueIs(ev, "grocery.list.1", "two")
			})

			Convey("can handle Replacement actions where the call site is a list", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  first:  2nd
  second: 1st
sorted:
  list:
    - (( grab meta.second ))
    - (( grab meta.first ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "sorted.list.0", "1st")
				valueIs(ev, "sorted.list.1", "2nd")
			})

			Convey("can handle Replacement actions where the call site is inside of a list", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  prod: production
  sandbox: sb322
boxen:
  - name: www
    env: (( grab meta.prod ))
  - name: wwwtest
    env: (( grab meta.sandbox ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "boxen.www.env", "production")
				valueIs(ev, "boxen.wwwtest.env", "sb322")
			})

			Convey("can handle simple Inject actions", func() {
				ev := &Evaluator{
					Tree: YAML(
						`templates:
  www:
    HA: enabled
    DR: disabled
host:
  web1:
    type: www
    <<<: (( inject templates.www ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "host.web1.HA", "enabled")
				valueIs(ev, "host.web1.DR", "disabled")
				valueIs(ev, "host.web1.type", "www")
				notPresent(ev, "host.web1.<<<")
			})

			Convey("can handle Inject actions where call site is in a list", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  jobs:
    api:
      template: api
    worker:
      template: worker
    db:
      template: database

jobs:
  - name: api_z1
    <<<: (( inject meta.jobs.api ))
  - name: api_z2
    <<<: (( inject meta.jobs.api ))

  - name: worker_z3
    <<<: (( inject meta.jobs.worker ))

  - name: db_z3
    <<<: (( inject meta.jobs.db ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "jobs.api_z1.template", "api")
				valueIs(ev, "jobs.api_z2.template", "api")

				valueIs(ev, "jobs.db_z3.template", "database")

				valueIs(ev, "jobs.worker_z3.template", "worker")
			})

			Convey("preserves call-site keys on conflict in an Inject scenario", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  template:
    foo: FOO
    bar: BAR

example:
  <<<: (( inject meta.template ))
  foo: foooo
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "example.foo", "foooo")
				valueIs(ev, "example.bar", "BAR")
			})

			Convey("merges sub-trees common to inject site and injected values", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  template:
    properties:
      foo: bar
      baz: NOT-OVERRIDDEN

thing:
  <<<: (( inject meta.template ))
  properties:
    bar: baz
    baz: overridden
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "thing.properties.bar", "baz")
				valueIs(ev, "thing.properties.baz", "overridden")
				valueIs(ev, "thing.properties.foo", "bar")
			})

			Convey("uses deep-copy semantics to handle overrides correctly on template re-use", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  template:
    properties:
      key: DEFAULT
      sub:
        key: DEFAULT
foo:
  <<<: (( inject meta.template ))
  properties:
    key: FOO
    sub:
      key: FOO
bar:
  <<<: (( inject meta.template ))
  properties:
    key: BAR
    sub:
      key: BAR
boz:
  <<<: (( inject meta.template ))
  properties:
    key: BOZ
    sub:
      key: BOZ
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "foo.properties.key", "FOO")
				valueIs(ev, "foo.properties.sub.key", "FOO")

				valueIs(ev, "bar.properties.key", "BAR")
				valueIs(ev, "bar.properties.sub.key", "BAR")

				valueIs(ev, "boz.properties.key", "BOZ")
				valueIs(ev, "boz.properties.sub.key", "BOZ")
			})

			Convey("uses deep-copy semantics for re-use of injected templates with embedded lists", func() {
				ev := &Evaluator{
					Tree: YAML(
						`meta:
  template:
    properties:
      key: DEFAULT
      list:
        - key: DEFAULT
foo:
  <<<: (( inject meta.template ))
  properties:
    key: FOO
    list:
      - key: FOO
bar:
  <<<: (( inject meta.template ))
  properties:
    key: BAR
    list:
      - key: BAR
boz:
  <<<: (( inject meta.template ))
  properties:
    key: BOZ
    list:
      - key: BOZ
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "foo.properties.key", "FOO")
				valueIs(ev, "foo.properties.list[0].key", "FOO")

				valueIs(ev, "bar.properties.key", "BAR")
				valueIs(ev, "bar.properties.list[0].key", "BAR")

				valueIs(ev, "boz.properties.key", "BOZ")
				valueIs(ev, "boz.properties.list[0].key", "BOZ")
			})

			Convey("handles static_ips() call and a subsequent grab", func() {
				ev := &Evaluator{
					Tree: YAML(
						`jobs:
- name: api_z1
  instances: 1
  networks:
  - name: net1
    static_ips: (( static_ips(0, 1, 2) ))

networks:
- name: net1
  subnets:
    - static: [192.168.1.2 - 192.168.1.30]

properties:
  api_servers: (( grab jobs.api_z1.networks.net1.static_ips ))
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldBeNil)

				valueIs(ev, "jobs.api_z1.networks.net1.static_ips.0", "192.168.1.2")
				valueIs(ev, "properties.api_servers.0", "192.168.1.2")
			})

			Convey("handles allocation conflicts of static IP addresses", func() {
				ev := &Evaluator{
					Tree: YAML(
						`jobs:
- name: api_z1
  instances: 1
  networks:
  - name: net1
    static_ips: (( static_ips(0, 1, 2) ))
- name: api_z2
  instances: 1
  networks:
  - name: net1
    static_ips: (( static_ips(0, 1, 2) ))

networks:
- name: net1
  subnets:
    - static: [192.168.1.2 - 192.168.1.30]
`),
				}

				err := ev.DataFlow()
				So(err, ShouldBeNil)

				err = ev.Patch()
				So(err, ShouldNotBeNil)
			})
		})
	})
}
Ejemplo n.º 9
0
func TestEvaluator(t *testing.T) {
	YAML := func(s string) map[interface{}]interface{} {
		y, err := simpleyaml.NewYaml([]byte(s))
		So(err, ShouldBeNil)

		data, err := y.Map()
		So(err, ShouldBeNil)

		return data
	}
	ToYAML := func(tree map[interface{}]interface{}) string {
		y, err := yaml.Marshal(tree)
		So(err, ShouldBeNil)
		return string(y)
	}
	ReYAML := func(s string) string {
		return ToYAML(YAML(s))
	}
	RunPhaseTests := func(phase OperatorPhase, src string) {
		var test, input, dataflow, output string
		var current *string
		testPat := regexp.MustCompile(`^##+\s+(.*)\s*$`)

		convey := func() {
			if test != "" {
				Convey(test, func() {
					ev := &Evaluator{Tree: YAML(input)}

					ops, err := ev.DataFlow(phase)
					So(err, ShouldBeNil)

					// map data flow ops into 'dataflow:' YAML list
					var flow []map[string]string
					for _, op := range ops {
						flow = append(flow, map[string]string{op.where.String(): op.src})
					}
					So(ToYAML(map[interface{}]interface{}{"dataflow": flow}),
						ShouldEqual, ReYAML(dataflow))

					err = ev.RunPhase(phase)
					So(err, ShouldBeNil)
					So(ToYAML(ev.Tree), ShouldEqual, ReYAML(output))
				})
			}
		}

		s := bufio.NewScanner(strings.NewReader(src))
		for s.Scan() {
			if testPat.MatchString(s.Text()) {
				m := testPat.FindStringSubmatch(s.Text())
				convey()
				test, input, dataflow, output = m[1], "", "", ""
				continue
			}

			if s.Text() == "---" {
				if input == "" {
					current = &input
				} else if dataflow == "" {
					current = &dataflow
				} else {
					current = &output
				}
				continue
			}

			if current != nil {
				*current = *current + s.Text() + "\n"
			}
		}
		convey()
	}

	/*
	   ##     ## ######## ########   ######   ########
	   ###   ### ##       ##     ## ##    ##  ##
	   #### #### ##       ##     ## ##        ##
	   ## ### ## ######   ########  ##   #### ######
	   ##     ## ##       ##   ##   ##    ##  ##
	   ##     ## ##       ##    ##  ##    ##  ##
	   ##     ## ######## ##     ##  ######   ########
	*/

	Convey("Merge Phase", t, func() {
		RunPhaseTests(MergePhase, `
##################################################   can handle simplest case
---
templates:
  www:
    HA: enabled
    DR: disabled
host:
  web1:
    type: www
    <<<: (( inject templates.www ))

---
dataflow:
- host.web1.<<<: (( inject templates.www ))

---
templates:
  www:
    HA: enabled
    DR: disabled
host:
  web1:
    type: www
    HA: enabled
    DR: disabled


################################################   ignores EvalPhase operators
---
meta:
  template:
    check: (( param "stuff" ))
    baz: (( grab meta.template.foo ))
    str: (( concat "foo" meta.template.baz ))
    foo: bar

thing:
  <<<: (( inject meta.template ))

---
dataflow:
- thing.<<<: (( inject meta.template ))

---
meta:
  template:
    check: (( param "stuff" ))
    baz: (( grab meta.template.foo ))
    str: (( concat "foo" meta.template.baz ))
    foo: bar

thing:
  check: (( param "stuff" ))
  baz: (( grab meta.template.foo ))
  str: (( concat "foo" meta.template.baz ))
  foo: bar


############################################   handles nested (( inject ... )) calls
---
meta:
  template1:
    foo: bar
    baz: (( grab meta.template1.foo ))
  template2:
    <<<: (( inject meta.template1 ))
    xyzzy: nothing happens

thing:
  <<<: (( inject meta.template2 ))

---
dataflow:
- meta.template2.<<<: (( inject meta.template1 ))
- thing.<<<:          (( inject meta.template2 ))

---
meta:
  template1:
    foo: bar
    baz: (( grab meta.template1.foo ))
  template2:
    foo: bar
    baz: (( grab meta.template1.foo ))
    xyzzy: nothing happens

thing:
  foo: bar
  baz: (( grab meta.template1.foo ))
  xyzzy: nothing happens

############################################   handles nested (( inject ... )) through array aliasing

---
a:
  foo: bar

jobs:
  - name: b
    <<<: (( inject a ))
    boz: baz

c:
  <<<: (( inject jobs.b ))
  xy: zzy

---
dataflow:
- jobs.b.<<<: (( inject a ))
- c.<<<: (( inject jobs.b ))

---
a:
  foo: bar

jobs:
  - name: b
    foo: bar
    boz: baz

c:
  name: b
  foo: bar
  boz: baz
  xy: zzy



#########################################################   handles Inject into a list
---
meta:
  jobs:
    api:
      template: api
    worker:
      template: worker
    db:
      template: database

jobs:
  - name: api_z1
    <<<: (( inject meta.jobs.api ))
  - name: api_z2
    <<<: (( inject meta.jobs.api ))

  - name: worker_z3
    <<<: (( inject meta.jobs.worker ))

  - name: db_z3
    <<<: (( inject meta.jobs.db ))

---
dataflow:
- jobs.api_z1.<<<: (( inject meta.jobs.api ))
- jobs.api_z2.<<<: (( inject meta.jobs.api ))
- jobs.worker_z3.<<<: (( inject meta.jobs.worker ))
- jobs.db_z3.<<<: (( inject meta.jobs.db ))

---
meta:
  jobs:
    api:
      template: api
    worker:
      template: worker
    db:
      template: database

jobs:
  - name: api_z1
    template: api
  - name: api_z2
    template: api

  - name: worker_z3
    template: worker

  - name: db_z3
    template: database


#################################   preserves call-site keys on conflict in an Inject scenario
---
meta:
  template:
    foo: FOO
    bar: BAR

example:
  <<<: (( inject meta.template ))
  foo: foooo

---
dataflow:
- example.<<<: (( inject meta.template ))

---
meta:
  template:
    foo: FOO
    bar: BAR

example:
  foo: foooo
  bar: BAR


#############################   merges sub-tress common to inject sites and injected values
---
meta:
  template:
    properties:
      foo: bar
      baz: NOT-OVERRIDDEN

thing:
  <<<: (( inject meta.template ))
  properties:
    bar: baz
    baz: overridden

---
dataflow:
- thing.<<<: (( inject meta.template ))

---
meta:
  template:
    properties:
      foo: bar
      baz: NOT-OVERRIDDEN

thing:
  properties:
    foo: bar
    bar: baz
    baz: overridden

#################   merges name-indexed sub-arrays properly between call-site and inject site
---
meta:
  api_node:
    templates:
    - name: my_job
      release: my_release
    - name: my_other_job
      release: my_other_release
    properties:
      foo: bar
jobs:
  api_node:
    .: (( inject meta.api_node ))
    properties:
      this: that
    templates:
    - name: my_superspecial_job
      release: my_superspecial_release

---
dataflow:
- jobs.api_node..: (( inject meta.api_node ))

---
meta:
  api_node:
    templates:
    - name: my_job
      release: my_release
    - name: my_other_job
      release: my_other_release
    properties:
      foo: bar
jobs:
  api_node:
    properties:
      foo: bar
      this: that
    templates:
    - name: my_job
      release: my_release
    - name: my_other_job
      release: my_other_release
    - name: my_superspecial_job
      release: my_superspecial_release


#################   uses deep-copy semantics to handle overrides correctly on template re-use
---
meta:
  template:
    properties:
      key: DEFAULT
      sub:
        key: DEFAULT
foo:
  <<<: (( inject meta.template ))
  properties:
    key: FOO
    sub:
      key: FOO
bar:
  <<<: (( inject meta.template ))
  properties:
    key: BAR
    sub:
      key: BAR
boz:
  <<<: (( inject meta.template ))
  properties:
    key: BOZ
    sub:
      key: BOZ

---
dataflow:
- bar.<<<: (( inject meta.template ))
- boz.<<<: (( inject meta.template ))
- foo.<<<: (( inject meta.template ))

---
meta:
  template:
    properties:
      key: DEFAULT
      sub:
        key: DEFAULT
foo:
  properties:
    key: FOO
    sub:
      key: FOO
bar:
  properties:
    key: BAR
    sub:
      key: BAR
boz:
  properties:
    key: BOZ
    sub:
      key: BOZ


#########################################################  appends to injected arrays
---
meta:
  job:
    templates:
      - first
      - second
foo:
  <<<: (( inject meta.job ))
  templates:
    - third

---
dataflow:
- foo.<<<: (( inject meta.job ))

---
meta:
  job:
    templates:
      - first
      - second
foo:
  templates:
    - first
    - second
    - third
`)
	})

	Convey("Merge Phase Error Detection", t, func() {
		Convey("detects direct (a -> b -> a) cycles in data flow graph", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  bar:
    <<<: (( inject meta.foo ))
  foo:
    <<<: (( inject meta.bar ))
`),
			}

			_, err := ev.DataFlow(MergePhase)
			So(err, ShouldNotBeNil)
		})
	})

	/*
	   ######## ##     ##    ###    ##
	   ##       ##     ##   ## ##   ##
	   ##       ##     ##  ##   ##  ##
	   ######   ##     ## ##     ## ##
	   ##        ##   ##  ######### ##
	   ##         ## ##   ##     ## ##
	   ########    ###    ##     ## ########
	*/

	Convey("Eval Phase", t, func() {
		RunPhaseTests(EvalPhase, `
#############################################################   handles simple expressions
---
foo: (( concat "foo" ":" "bar" ))

---
dataflow:
- foo: (( concat "foo" ":" "bar" ))

---
foo: foo:bar


####################################################   handles simple reference expressions
---
meta:
  domain: foo.bar
domain: (( grab meta.domain ))

---
dataflow:
- domain: (( grab meta.domain ))

---
meta:
  domain: foo.bar
domain: foo.bar


#########################################   handles simple reference-or-literal expressions
---
meta:
  env: prod
domain:    (( grab meta.domain || "default-domain" ))
env:       (( grab meta.env || "sandbox" ))
instances: (( grab meta.size || 42 ))
nice:      (( grab meta.nice || -5 ))
pi:        (( grab math.CONSTANTS.pi || 3.14159 ))
delta:     (( grab meta.delta || .001 ))
secure:    (( grab meta.secure || true ))
online:    (( grab meta.online || false ))

---
dataflow:
- delta:     (( grab meta.delta || .001 ))
- domain:    (( grab meta.domain || "default-domain" ))
- env:       (( grab meta.env || "sandbox" ))
- instances: (( grab meta.size || 42 ))
- nice:      (( grab meta.nice || -5 ))
- online:    (( grab meta.online || false ))
- pi:        (( grab math.CONSTANTS.pi || 3.14159 ))
- secure:    (( grab meta.secure || true ))

---
meta:
  env: prod
domain: default-domain
env: prod
instances: 42
nice: -5
pi: 3.14159
delta: 0.001
secure: true
online: false


#####################################   handles true, TRUE, and True as boolean keywords
---
TrUe: sure
things:
- (( grab meta.enoent || true ))
- (( grab meta.enoent || TRUE ))
- (( grab meta.enoent || True ))
- (( grab meta.enoent || TrUe ))

---
dataflow:
- things.0: (( grab meta.enoent || true ))
- things.1: (( grab meta.enoent || TRUE ))
- things.2: (( grab meta.enoent || True ))
- things.3: (( grab meta.enoent || TrUe ))

---
TrUe: sure
things:
- true
- true
- true
- sure


#####################################   handles false, FALSE, and False as boolean keywords
---
FaLSe: why not?
things:
- (( grab meta.enoent || false ))
- (( grab meta.enoent || FALSE ))
- (( grab meta.enoent || False ))
- (( grab meta.enoent || FaLSe ))

---
dataflow:
- things.0: (( grab meta.enoent || false ))
- things.1: (( grab meta.enoent || FALSE ))
- things.2: (( grab meta.enoent || False ))
- things.3: (( grab meta.enoent || FaLSe ))

---
FaLSe: why not?
things:
- false
- false
- false
- why not?


######################   handles ~, nil, Nil, NIL, null, Null, and NULL as the nil keyword
---
NuLL: 4 (haha ruby joke)
things:
- (( grab meta.enoent || ~ ))
- (( grab meta.enoent || nil ))
- (( grab meta.enoent || Nil ))
- (( grab meta.enoent || NIL ))
- (( grab meta.enoent || null ))
- (( grab meta.enoent || Null ))
- (( grab meta.enoent || NULL ))
- (( grab meta.enoent || NuLL ))

---
dataflow:
- things.0: (( grab meta.enoent || ~ ))
- things.1: (( grab meta.enoent || nil ))
- things.2: (( grab meta.enoent || Nil ))
- things.3: (( grab meta.enoent || NIL ))
- things.4: (( grab meta.enoent || null ))
- things.5: (( grab meta.enoent || Null ))
- things.6: (( grab meta.enoent || NULL ))
- things.7: (( grab meta.enoent || NuLL ))

---
NuLL: 4 (haha ruby joke)
things:
- null
- null
- null
- null
- null
- null
- null
- 4 (haha ruby joke)




#########################################   handles simple reference-or-nil expressions
---
domain: (( grab meta.domain || nil ))
env:    (( grab meta.env || ~ ))
site:   (( grab meta.site || null ))

---
dataflow:
- domain: (( grab meta.domain || nil ))
- env:    (( grab meta.env || ~ ))
- site:   (( grab meta.site || null ))

---
domain: ~
env: ~
site: ~


##################################   stops at the first concrete (possibly false) expression
---
meta:
  other: FAIL

#
# should stop here ----------.   (because it's resolvable, even if it
#                            |    evaluates to a traditionally non-true value)
#                            v
foo: (( grab meta.enoent || false || meta.other || "failed" ))

---
dataflow:
- foo: (( grab meta.enoent || false || meta.other || "failed" ))

---
meta:
  other: FAIL
foo: false


##################################   stops at the first concrete (possibly 0) expression
---
meta:
  other: FAIL

#
# should stop here ----------.   (because it's resolvable, even if it
#                            |    evaluates to a traditionally non-true value)
#                            v
foo: (( grab meta.enoent || 0 || meta.other || "failed" ))

---
dataflow:
- foo: (( grab meta.enoent || 0 || meta.other || "failed" ))

---
meta:
  other: FAIL
foo: 0


##################################   stops at the first concrete (possibly nil) expression
---
meta:
  other: FAIL

#
# should stop here ----------.   (because it's resolvable, even if it
#                            |    evaluates to a traditionally non-true value)
#                            v
foo: (( grab meta.enoent || nil || meta.other || "failed" ))

---
dataflow:
- foo: (( grab meta.enoent || nil || meta.other || "failed" ))

---
meta:
  other: FAIL
foo: ~


###############################################  handles concrete expression in the middle
---
meta:
  second: SECOND
foo: (( grab meta.first || meta.second || "unspecified" ))

---
dataflow:
- foo: (( grab meta.first || meta.second || "unspecified" ))

---
meta:
  second: SECOND
foo: SECOND


#################################   skips short-circuited alternates in Data Flow analysis
---
meta:
  foo: FOO
  bar: (( grab meta.foo ))
  boz: (( grab meta.foo ))

foo: (( grab meta.bar || "foo?" || meta.boz ))
# NOTE: meta.boz in $.foo is exempt from DFA, because the "foo?" literal
#       will *always* stop evaluation of the expression

bar: (( grab meta.xyzzy || "bar?" || meta.boz ))
# NOTE: same with $.bar; meta.boz is not in play

---
dataflow:
- bar: (( grab meta.xyzzy || "bar?" || meta.boz ))
- meta.bar: (( grab meta.foo ))
- meta.boz: (( grab meta.foo ))
- foo: (( grab meta.bar || "foo?" || meta.boz ))

---
meta:
  foo: FOO
  bar: FOO
  boz: FOO
foo: FOO
bar: bar?


#####################################   handles Data Flow dependencies for all expressions
---
meta:
  domain: example.com
  web: (( concat "www.", meta.domain || "sandbox.example.com" ))
api:
  endpoint: (( grab meta.web || meta.domain || ~ ))

---
dataflow:
- meta.web: (( concat "www.", meta.domain || "sandbox.example.com" ))
- api.endpoint: (( grab meta.web || meta.domain || ~ ))

---
meta:
  domain: example.com
  web: www.example.com
api:
  endpoint: www.example.com


###############################  handles indirect addressing of lists-of-maps in Data Flow
---
a:  (( grab b.squad.value ))
b:
  - name: squad
    value: (( grab c.value ))
c:
  value: VALUE
d: (( grab b.squad.value ))
e: (( grab c.value ))

---
dataflow:
- b.squad.value: (( grab c.value ))
- e:         (( grab c.value ))
- a:         (( grab b.squad.value ))
- d:         (( grab b.squad.value ))

---
a: VALUE
b:
  - name: squad
    value: VALUE
c:
  value: VALUE
d: VALUE
e: VALUE


#################################   handles multiple space-separated or-operands properly
---
meta:
  foo: FOO
  bar: BAR
foobar: (( concat meta.foo || "foo"  meta.bar || "bar" ))
fooboz: (( concat meta.foo || "foo"  meta.boz || "boz" ))

---
dataflow:
- foobar: (( concat meta.foo || "foo"  meta.bar || "bar" ))
- fooboz: (( concat meta.foo || "foo"  meta.boz || "boz" ))

---
meta:
  foo: FOO
  bar: BAR
foobar: FOOBAR
fooboz: FOOboz


#################################   handles multiple comma-seaprated or-operands properly
---
meta:
  foo: FOO
  bar: BAR
foobar: (( concat meta.foo || "foo", meta.bar || "bar" ))
fooboz: (( concat meta.foo || "foo", meta.boz || "boz" ))

---
dataflow:
- foobar: (( concat meta.foo || "foo", meta.bar || "bar" ))
- fooboz: (( concat meta.foo || "foo", meta.boz || "boz" ))

---
meta:
  foo: FOO
  bar: BAR
foobar: FOOBAR
fooboz: FOOboz


############################################   can handle simple map-based Replace actions
---
meta:
  domain: sandbox.example.com
  web: (( grab meta.domain ))
urls:
  home: (( concat "http://www." meta.web ))

---
dataflow:
- meta.web: (( grab meta.domain ))
- urls.home: (( concat "http://www." meta.web ))

---
meta:
  domain: sandbox.example.com
  web: sandbox.example.com
urls:
  home: http://www.sandbox.example.com


############################   can handle Replacement actions where the new value is a list
---
meta:
  things:
    - one
    - two
grocery:
  list: (( grab meta.things ))

---
dataflow:
- grocery.list: (( grab meta.things ))

---
meta:
  things:
    - one
    - two
grocery:
  list:
    - one
    - two


############################   can handle Replacement actions where the call site is a list
---
meta:
  first:  2nd
  second: 1st
sorted:
  list:
    - (( grab meta.second ))
    - (( grab meta.first ))

---
dataflow:
- sorted.list.0: (( grab meta.second ))
- sorted.list.1: (( grab meta.first ))

---
meta:
  first:  2nd
  second: 1st
sorted:
  list:
    - 1st
    - 2nd


###################   can handle Replacement actions where the call site is inside of a list
---
meta:
  prod: production
  sandbox: sb322
boxen:
  - name: www
    env: (( grab meta.prod ))
  - name: wwwtest
    env: (( grab meta.sandbox ))

---
dataflow:
- boxen.www.env: (( grab meta.prod ))
- boxen.wwwtest.env: (( grab meta.sandbox ))

---
meta:
  prod: production
  sandbox: sb322
boxen:
  - name: www
    env: production
  - name: wwwtest
    env: sb322


########################################   handles sequentially dependent (( grab ... )) calls
---
meta:
  foo:  FOO
  bar:  (( grab meta.foo ))
  baz:  (( grab meta.bar ))
  quux: (( grab meta.baz ))
  boz:  (( grab meta.quux ))

---
dataflow:
- meta.bar:   (( grab meta.foo ))
- meta.baz:   (( grab meta.bar ))
- meta.quux:  (( grab meta.baz ))
- meta.boz:   (( grab meta.quux ))

---
meta:
  foo:  FOO
  bar:  FOO
  baz:  FOO
  quux: FOO
  boz:  FOO


################################   handles sequentially dependent calls, regardless of operator
---
meta:
  foo: FOO
  bar: (( grab meta.foo ))
  baz: (( grab meta.bar ))
  quux: (( concat "literal:" meta.baz ))
  boz: (( grab meta.quux ))

---
dataflow:
- meta.bar:  (( grab meta.foo ))
- meta.baz:  (( grab meta.bar ))
- meta.quux: (( concat "literal:" meta.baz ))
- meta.boz:  (( grab meta.quux ))

---
meta:
  foo: FOO
  bar: FOO
  baz: FOO
  quux: literal:FOO
  boz:  literal:FOO


##############################################   handles operators dependencies inside of lists
---
meta:
  - FOO
  - (( grab meta.0 ))
  - (( grab meta.1 ))
  - (( concat "literal:" meta.2 ))
  - (( grab meta.3 ))

---
dataflow:
- meta.1:  (( grab meta.0 ))
- meta.2:  (( grab meta.1 ))
- meta.3:  (( concat "literal:" meta.2 ))
- meta.4:  (( grab meta.3 ))

---
meta:
  - FOO
  - FOO
  - FOO
  - literal:FOO
  - literal:FOO


#################################################   handles deep copy in data flow graph
---
meta:
  first: [ a, b, c ]
  second: (( grab meta.first ))
  third:  (( grab meta.second ))
  gotcha: (( grab meta.third.0 ))

---
dataflow:
- meta.second: (( grab meta.first ))
- meta.third:  (( grab meta.second ))
- meta.gotcha: (( grab meta.third.0 ))

# (the key point here is that meta.third.0 doesn't exist in the tree until we start
# evaluating, but we still need to get the order correct; we should have a dep on
# meta.third, and hope that run-time resolution puts an array there for us to find...)

---
meta:
  first:  [ a, b, c ]
  second: [ a, b, c ]
  third:  [ a, b, c ]
  gotcha:   a


###############################  handles implicit static_ip dependency on networks.*.name
---
meta:
  net: real
  environment: prod
  size: 4
networks:
  - name: (( concat meta.net "-prod" ))
    subnets:
    - static: [ 10.0.0.5 - 10.0.0.100 ]
jobs:
  - name: job1
    instances: 4
    networks:
      - name: real-prod # must be literal to avoid non-determinism
        static_ips: (( static_ips 1 2 3 4 ))

---
dataflow:
- networks.0.name:                         (( concat meta.net "-prod" ))
- jobs.job1.networks.real-prod.static_ips: (( static_ips 1 2 3 4 ))

---
meta:
  net: real
  environment: prod
  size: 4
networks:
  - name: real-prod
    subnets:
    - static: [ 10.0.0.5 - 10.0.0.100 ]
jobs:
  - name: job1
    instances: 4
    networks:
      - name: real-prod
        static_ips:
            # skip IP[0]!
          - 10.0.0.6
          - 10.0.0.7
          - 10.0.0.8
          - 10.0.0.9


####################   handles implicit static_ip dependency on jobs.*.networks.*.name
---
meta:
  environment: prod
  size: 4
networks:
  - name: sandbox
    subnets:
    - static: [ 10.2.0.5 - 10.2.0.10 ]
  - name: prod
    subnets:
    - static: [ 10.0.0.5 - 10.0.0.100 ]
jobs:
  - name: job1
    instances: 4
    networks:
      - name: (( grab meta.environment ))
        static_ips: (( static_ips 1 2 3 4 ))

---
dataflow:
- jobs.job1.networks.0.name:        (( grab meta.environment ))
- jobs.job1.networks.0.static_ips:  (( static_ips 1 2 3 4 ))

---
meta:
  environment: prod
  size: 4
networks:
  - name: sandbox
    subnets:
    - static: [ 10.2.0.5 - 10.2.0.10 ]
  - name: prod
    subnets:
    - static: [ 10.0.0.5 - 10.0.0.100 ]
jobs:
  - name: job1
    instances: 4
    networks:
      - name: prod
        static_ips:
            # skip IP[0]!
          - 10.0.0.6
          - 10.0.0.7
          - 10.0.0.8
          - 10.0.0.9


##########################   handles (( static_ips ... )) call and a subsequent (( grab ... ))
---
jobs:
- name: api_z1
  instances: 1
  networks:
  - name: net1
    static_ips: (( static_ips(0, 1, 2) ))

networks:
- name: net1
  subnets:
    - static: [192.168.1.2 - 192.168.1.30]

properties:
  api_servers: (( grab jobs.api_z1.networks.net1.static_ips ))

---
dataflow:
- jobs.api_z1.networks.net1.static_ips: (( static_ips(0, 1, 2) ))
- properties.api_servers:       (( grab jobs.api_z1.networks.net1.static_ips ))

---
jobs:
- name: api_z1
  instances: 1
  networks:
  - name: net1
    static_ips:
    - 192.168.1.2

networks:
- name: net1
  subnets:
    - static: [192.168.1.2 - 192.168.1.30]

properties:
  api_servers:
    - 192.168.1.2


#############################################  handles static_ip across multiple static ranges
---
jobs:
- name: api_z1
  instances: 4
  networks:
  - name: net1
    static_ips: (( static_ips 0 1 2 3 ))

networks:
- name: net1
  subnets:
    - static:
      - 10.0.0.2 - 10.0.0.3      #  2 ips
      - 10.0.0.90                # +1
      - 10.0.0.100 - 10.0.0.103  # +4

---
dataflow:
- jobs.api_z1.networks.net1.static_ips: (( static_ips 0 1 2 3 ))

---
jobs:
- name: api_z1
  instances: 4
  networks:
  - name: net1
    static_ips:
    - 10.0.0.2
    - 10.0.0.3
    - 10.0.0.90
    - 10.0.0.100

networks:
- name: net1
  subnets:
    - static:
      - 10.0.0.2 - 10.0.0.3      #  2 ips
      - 10.0.0.90                # +1
      - 10.0.0.100 - 10.0.0.103  # +4


#########################################  Basic test of (( cartesian-product .... )) operator
---
meta:
  hosts:
    - a.example.com
    - b.example.com
    - c.example.com
  port: 8088

hosts: (( cartesian-product meta.hosts ":" meta.port ))

---
dataflow:
- hosts: (( cartesian-product meta.hosts ":" meta.port ))

---
meta:
  hosts:
    - a.example.com
    - b.example.com
    - c.example.com
  port: 8088

hosts:
  - a.example.com:8088
  - b.example.com:8088
  - c.example.com:8088


####################################  (( cartesian-product .... )) of an empty component array
---
meta:
  hosts: []
  port: 8088

hosts: (( cartesian-product meta.hosts ":" meta.port ))
ports: (( cartesian-product meta.port  ":" meta.hosts ))

---
dataflow:
- hosts: (( cartesian-product meta.hosts ":" meta.port ))
- ports: (( cartesian-product meta.port  ":" meta.hosts ))

---
meta:
  hosts: []
  port: 8088

hosts: []
ports: []


##########################################################  1-ary (( cartesian-product .... ))
---
meta:
  - [a, b, c]

all: (( cartesian-product meta[0] ))

---
dataflow:
- all: (( cartesian-product meta[0] ))

---
meta:
  - [a, b, c]
all: [a, b, c]


##########################################################  n-ary (( cartesian-product .... ))
---
meta:
  - [a, b, c]
  - [1, 2, 3]
  - [x, 'y', z]

all: (( cartesian-product meta[0] meta[1] meta[2] ))

---
dataflow:
- all: (( cartesian-product meta[0] meta[1] meta[2] ))

---
meta:
  - [a, b, c]
  - [1, 2, 3]
  - [x, 'y', z]
all:
  - a1x
  - a1y
  - a1z
  - a2x
  - a2y
  - a2z
  - a3x
  - a3y
  - a3z
  - b1x
  - b1y
  - b1z
  - b2x
  - b2y
  - b2z
  - b3x
  - b3y
  - b3z
  - c1x
  - c1y
  - c1z
  - c2x
  - c2y
  - c2z
  - c3x
  - c3y
  - c3z


###########################################  (( cartesian-product ... )) with grab'd arguments
---
meta:
  first: [a, b]
  second: [1, 2]
  third:  (( grab meta.second ))

all: (( cartesian-product meta.first "," meta.third ))

---
dataflow:
- meta.third: (( grab meta.second ))
- all: (( cartesian-product meta.first "," meta.third ))

---
meta:
  first:  [a, b]
  second: [1, 2]
  third:  [1, 2]

all:
  - a,1
  - a,2
  - b,1
  - b,2

###################################  (( cartesian-product ... )) with grab'd sublist arguments
---
meta:
  first: [a, b]
  second:
    - x
    - (( grab meta.first[0] ))

all: (( cartesian-product meta.first "," meta.second ))

---
dataflow:
- meta.second.1: (( grab meta.first[0] ))
- all: (( cartesian-product meta.first "," meta.second ))

---
meta:
  first:  [a, b]
  second: [x, a]

all:
  - a,x
  - a,a
  - b,x
  - b,a


###########################################  can extract keys via the (( keys ... )) operator
---
meta:
  config:
    first: this is the first value
    second:
      value: the second
keys: (( keys meta.config ))

---
dataflow:
- keys: (( keys meta.config ))

---
meta:
  config:
    first: this is the first value
    second:
      value: the second
keys:
  - first
  - second


###########################################  can extract keys from multiple maps
---
meta:
  config:
    first: this is the first value
    second:
      value: the second
  alt:
    third: third config
keys: (( keys meta.config meta.alt ))

---
dataflow:
- keys: (( keys meta.config meta.alt ))

---
meta:
  config:
    first: this is the first value
    second:
      value: the second
  alt:
    third: third config
keys:
  - first
  - second
  - third

#################################### (( join ... )) an array with (( grab ...))
---
greeting: hello

z:
- (( grab greeting ))
- world

output: (( join " " z ))
---
dataflow:
- z.0: (( grab greeting ))
- output: (( join " " z ))
---
greeting: hello
output: hello world
z:
- hello
- world
#################################### (( join ... )) an array with several (( grab ...))s
---
greeting: hello
greeting2: world

z:
- (( grab greeting ))
- (( grab greeting2 ))

output: (( join " " z ))
---
dataflow:
- z.0: (( grab greeting ))
- z.1: (( grab greeting2 ))
- output: (( join " " z ))
---
greeting: hello
greeting2: world
output: hello world
z:
- hello
- world
#################################### (( join ... )) a string reference with a grab
---
greeting: hello
z_one: (( grab greeting ))
z_two: world
output:
- (( join " " z_one z_two ))
---
dataflow:
- z_one: (( grab greeting ))
- output.0: (( join " " z_one z_two ))
---
greeting: hello
output:
- hello world
z_one: hello
z_two: world


################################################   basic escape sequence handling
---
test: ""
cr:   (( concat test "a\rb" ))
nl:   (( concat test "a\nb" ))
tab:  (( concat test "a\tb" ))
back: (( concat test "a\\b" ))
dq:   (( concat test "a\"b" ))
sq:   (( concat test "a\'b" ))

---
dataflow:
- back: (( concat test "a\\b" ))
- cr:   (( concat test "a\rb" ))
- dq:   (( concat test "a\"b" ))
- nl:   (( concat test "a\nb" ))
- sq:   (( concat test "a\'b" ))
- tab:  (( concat test "a\tb" ))

---
test: ""
cr:   "a\rb"
nl:   "a\nb"
tab:  "a\tb"
back: a\b
dq:   'a"b'
sq:   "a'b"


#############################################   repeated escape sequence handling
---
compound: (( concat "Line1\nLine2\nLine3" "\n" "Line4\ttabbed\n" ))

---
dataflow:
- compound: (( concat "Line1\nLine2\nLine3" "\n" "Line4\ttabbed\n" ))

---
compound: |
  Line1
  Line2
  Line3
  Line4	tabbed


########################################   concat certs with newlines (escape seq)
---
cert: |-
  -- BEGIN CERT --
  unei3Eet2mahbou8
  weiXi7choo7ufei8
  --- END CERT ---

key: |-
  -- BEGIN KEY ---
  chaev0Gai3Baedul
  noithaifu0ree0Ka
  shoowuBaoti4chee
  -- END KEY -----

combined: (( concat cert "\n" key "\n" ))

---
dataflow:
- combined: (( concat cert "\n" key "\n" ))

---
cert: |-
  -- BEGIN CERT --
  unei3Eet2mahbou8
  weiXi7choo7ufei8
  --- END CERT ---

key: |-
  -- BEGIN KEY ---
  chaev0Gai3Baedul
  noithaifu0ree0Ka
  shoowuBaoti4chee
  -- END KEY -----

combined: |
  -- BEGIN CERT --
  unei3Eet2mahbou8
  weiXi7choo7ufei8
  --- END CERT ---
  -- BEGIN KEY ---
  chaev0Gai3Baedul
  noithaifu0ree0Ka
  shoowuBaoti4chee
  -- END KEY -----

`)
	})

	Convey("Eval Phase Error Detection", t, func() {
		Convey("detects direct (a -> b -> a) cycles in data flow graph", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  bar: (( grab meta.foo ))
  foo: (( grab meta.bar ))
`),
			}

			_, err := ev.DataFlow(EvalPhase)
			So(err, ShouldNotBeNil)
		})

		Convey("detects indirect (a -> b -> c -> a) cycles in data flow graph", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  foo: (( grab meta.bar ))
  bar: (( grab meta.baz ))
  baz: (( grab meta.foo ))
`),
			}

			_, err := ev.DataFlow(EvalPhase)
			So(err, ShouldNotBeNil)
		})

		Convey("detects indirect cycles created through operand data flow", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  foo: (( grab meta.bar ))
  bar: (( grab meta.baz ))
  baz: (( grab meta.enoent || meta.foo ))
`),
			}

			_, err := ev.DataFlow(EvalPhase)
			So(err, ShouldNotBeNil)
		})

		Convey("detects allocation conflicts of static IP addresses", func() {
			ev := &Evaluator{
				Tree: YAML(
					`jobs:
- name: api_z1
  instances: 1
  networks:
  - name: net1
    static_ips: (( static_ips(0, 1, 2) ))
- name: api_z2
  instances: 1
  networks:
  - name: net1
    static_ips: (( static_ips(0, 1, 2) ))

networks:
- name: net1
  subnets:
    - static: [192.168.1.2 - 192.168.1.30]
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
		})

		Convey("detects unsatisfied (( param )) inside of a (( grab ... )) call", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  key: (( param "you must specify this" ))
value: (( grab meta.key ))
`),
			}

			err := ev.RunPhase(ParamPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "1 error(s) detected")
			So(err.Error(), ShouldContainSubstring, "you must specify this")

			err = ev.RunPhase(EvalPhase)
			So(err, ShouldBeNil)
		})

		Convey("detects unsatisfied (( param )) inside of a (( concat ... )) call", func() {
			ev := &Evaluator{
				Tree: YAML(`
---
meta:
  key: (( param "you must specify this" ))
value: (( concat "key=" meta.key ))
`),
			}

			err := ev.RunPhase(ParamPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "1 error(s) detected")
			So(err.Error(), ShouldContainSubstring, "you must specify this")

			err = ev.RunPhase(EvalPhase)
			So(err, ShouldBeNil)
		})

		Convey("handles non-list (direct) args to (( cartesian-product ... ))", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  list: [a,b,c]
all: (( cartesian-product meta meta.list ))
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "cartesian-product operator only accepts arrays and string values")
		})

		Convey("treats list-of-lists args to (( cartesian-product ... )) as an error", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  list:
    - [a,b,c]
    - [d,e,f]
    - [g]
all: (( cartesian-product meta.list meta.list ))
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "cartesian-product operator can only operate on lists of scalar values")
		})

		Convey("treats list-of-maps args to (( cartesian-product ... )) as an error", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  list:
    - name: a
    - name: b
    - name: c
all: (( cartesian-product meta.list meta.list ))
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "cartesian-product operator can only operate on lists of scalar values")
		})

		Convey("(( cartesian-product ... )) requires an argument", func() {
			ev := &Evaluator{
				Tree: YAML(`
all: (( cartesian-product ))
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "no arguments specified to (( cartesian-product ... ))")
		})

		Convey("(( keys ... )) requires an argument", func() {
			ev := &Evaluator{
				Tree: YAML(`
keys: (( keys ))
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "no arguments specified to (( keys ... ))")
		})

		Convey("treats attempt to call (( keys ... )) on a literal as an error", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  test: is this a map?
keys: (( keys meta.test ))
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "$.keys: meta.test is not a map")
		})

		Convey("treats attempt to call (( keys ... )) on a list as an error", func() {
			ev := &Evaluator{
				Tree: YAML(`
meta:
  test:
    - but wait
    - this is not
    - a map...
keys: (( keys meta.test ))
`),
			}

			err := ev.RunPhase(EvalPhase)
			So(err, ShouldNotBeNil)
			So(err.Error(), ShouldContainSubstring, "$.keys: meta.test is not a map")
		})
	})
}