func mergeAllDocs(root map[interface{}]interface{}, paths []string) error { m := &Merger{} for _, path := range paths { DEBUG("Processing file '%s'", path) data, err := ioutil.ReadFile(path) if err != nil { return ansi.Errorf("@R{Error reading file} @m{%s}: %s\n", path, err.Error()) } if handleConcourseQuoting { data = quoteConcourse(data) } doc, err := parseYAML(data) if err != nil { return ansi.Errorf("@m{%s}: @R{%s}\n", path, err.Error()) } m.Merge(root, doc) tmpYaml, _ := yaml.Marshal(root) // we don't care about errors for debugging TRACE("Current data after processing '%s':\n%s", path, tmpYaml) } return m.Error() }
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) }) } }) }
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", ) }) }) }
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") }) }) }
func main() { var options struct { Debug bool `goptions:"-D, --debug, description='Enable debugging'"` Trace bool `goptions:"-T, --trace, description='Enable trace mode debugging (very verbose)'"` Version bool `goptions:"-v, --version, description='Display version information'"` Concourse bool `goptions:"--concourse, description='Pre/Post-process YAML for Concourse CI (handles {{ }} quoting)'"` Action goptions.Verbs Merge struct { Prune []string `goptions:"--prune, description='Specify keys to prune from final output (may be specified more than once)'"` CherryPick []string `goptions:"--cherry-pick, description='The opposite of prune, specify keys to cherry-pick from final output (may be specified more than once)'"` Files goptions.Remainder `goptions:"description='Merges file2.yml through fileN.yml on top of file1.yml'"` } `goptions:"merge"` JSON struct { Files goptions.Remainder `goptions:"description='Files to convert to JSON'"` } `goptions:"json"` } getopts(&options) if envFlag("DEBUG") || options.Debug { DebugOn = true } if envFlag("TRACE") || options.Trace { TraceOn = true DebugOn = true } handleConcourseQuoting = options.Concourse if options.Version { printfStdOut("%s - Version %s\n", os.Args[0], Version) exit(0) return } ansi.Color(isatty.IsTerminal(os.Stderr.Fd())) switch options.Action { case "merge": if len(options.Merge.Files) >= 1 { root := make(map[interface{}]interface{}) err := mergeAllDocs(root, options.Merge.Files) if err != nil { printfStdErr("%s\n", err.Error()) exit(2) return } ev := &Evaluator{Tree: root} err = ev.Run(options.Merge.Prune, options.Merge.CherryPick) if err != nil { printfStdErr("%s\n", err.Error()) exit(2) return } TRACE("Converting the following data back to YML:") TRACE("%#v", ev.Tree) merged, err := yaml.Marshal(ev.Tree) if err != nil { printfStdErr("Unable to convert merged result back to YAML: %s\nData:\n%#v", err.Error(), ev.Tree) exit(2) return } var output string if handleConcourseQuoting { output = dequoteConcourse(merged) } else { output = string(merged) } printfStdOut("%s\n", output) } else { usage() return } case "json": if len(options.JSON.Files) > 0 { jsons, err := JSONifyFiles(options.JSON.Files) if err != nil { printfStdErr("%s\n", err) exit(2) return } for _, output := range jsons { printfStdOut("%s\n", output) } } else { output, err := JSONifyIO(os.Stdin) if err != nil { printfStdErr("%s\n", err) exit(2) return } printfStdOut("%s\n", output) } default: usage() return } }